<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ParkDevDiary</title>
    <link>https://park-dev-diary.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 02:07:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JMDev</managingEditor>
    <image>
      <title>ParkDevDiary</title>
      <url>https://tistory1.daumcdn.net/tistory/6621316/attach/a8bfcdefe46c4424bde367486011678e</url>
      <link>https://park-dev-diary.tistory.com</link>
    </image>
    <item>
      <title>[Spark] serializer 를 통한 성능개선</title>
      <link>https://park-dev-diary.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spark를 최적화 할 수 있는 방법 중에는 직렬화 속성을 명시하면서 7% 성능을 높일 수 있었고, 더 많은 데이터를 처리하게 되면 이상의 성능을 높일 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화는 분산 애플리케이션의 성능에 중요한 역활을 합니다. 객체를 직렬화하는데 시간이 오래 걸리거나 많은 바이트를 소비하는 방식은 퍼포먼스를 늦추게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화는 객체를 바이트 스트림으로 변환하는 과정이며, 역직렬화는 바이트 스트림을 다시 객체로 변환하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfsgli/btsGcKvnubI/YiKo6LWaqy4iUGv5RErF7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfsgli/btsGcKvnubI/YiKo6LWaqy4iUGv5RErF7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfsgli/btsGcKvnubI/YiKo6LWaqy4iUGv5RErF7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdfsgli%2FbtsGcKvnubI%2FYiKo6LWaqy4iUGv5RErF7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;632&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 빨간색으로 강조한 부분이 Spark에서 RDD를 다른 분산 환경으로 네트워크를 통해 셔플링 될 때, 직렬화 된 데이터를 노드에게 전달합니다. 전달받은 노드에서는 직렬화된 데이터를 역직렬화하여 Object로써 활용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 흐름을 이해하였다면, 직렬화, 역직렬화를 방식이 성능에 영향을 끼칠 것이라는 것을 짐작할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Spark에서 직렬화는 어떠한 방식으로 이뤄질까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark에서 직렬화의 디폴트는 Java의 내장된 직렬화 기능을 사용합니다. 그리고 성능을 개선시킬 수 있는 Kryo직렬화 방식이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 내부 직렬화와 Kryo 직렬화 기능 두 기능의 차이점에 대해 알아보면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 직렬화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spark의 디폴트는 자바의 ObjectOutputStream 프레임워크를 사용하여 객체를 직렬화&lt;/li&gt;
&lt;li&gt;이 방법은 java.io.Serializable 을 구현하는 모든 클래스와 함께 동작&lt;/li&gt;
&lt;li&gt;java.io.Externalizable 을 확장함으로써 직렬화의 성능을 더 세밀하게 조정 가능&lt;/li&gt;
&lt;li&gt;Java 직렬화는 유연하지만 성능적으로는 느림&lt;/li&gt;
&lt;li&gt;하지만 많은 클래스에 대해 큰 직렬화 포맷을 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kryo 직렬화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spark는 Kryo 라이브러리를 사용하면 객체를 더 빠르게 직렬화할 수도 있음.&lt;/li&gt;
&lt;li&gt;Kryo는 Java 직렬화보다 훨씬 빠르고 압축률 높음.&lt;/li&gt;
&lt;li&gt;하지만 모든 Serializable 타입을 지원하지 않아, 미리 사용할 클래스가 등록되어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 데이터 포맷, 클래스 등록 및 필드 캐싱, 확장성으로 부터 두 직렬화 기능에 성능적인 차이가 발생한다고 볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kryo가 오류나는 특정한 상황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드상으로 Kryo 직렬화를 사용할 때 오류가 발생하는 순간을 확인하고 싶었고, 아래는 그러한 오류가 발생하는 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;class NonSerializableClass {
    private int value;

    public NonSerializableClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

public class SerializationErrorExample {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf().setAppName(&quot;SerializationErrorExample&quot;).setMaster(&quot;local&quot;);
        conf.set(&quot;spark.serializer&quot;, &quot;org.apache.spark.serializer.KryoSerializer&quot;);
        // 이 클래스를 Kryo에 등록하지 않음
        // conf.registerKryoClasses(new Class[]{NonSerializableClass.class});

        JavaSparkContext sc = new JavaSparkContext(conf);

        List&amp;lt;NonSerializableClass&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; 5; i++) {
            list.add(new NonSerializableClass(i));
        }

        JavaRDD&amp;lt;NonSerializableClass&amp;gt; rdd = sc.parallelize(list);
        // 액션을 수행하여 직렬화를 강제함
        System.out.println(rdd.count()); // 이 시점에서 직렬화 오류 발생 가능성

        sc.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 오류나지 않게 가능하려면 아래 직렬화 가능한 인터페이스를 implements 를 합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// Serializable 인터페이스를 구현하여 직렬화 가능하게 만듦
class SerializableClass implements Serializable {
    private int value;

    public SerializableClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

public class SerializationFixedExample {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf().setAppName(&quot;SerializationFixedExample&quot;).setMaster(&quot;local&quot;);
        conf.set(&quot;spark.serializer&quot;, &quot;org.apache.spark.serializer.KryoSerializer&quot;);
        // 클래스를 Kryo에 등록
        conf.registerKryoClasses(new Class[]{SerializableClass.class});

        JavaSparkContext sc = new JavaSparkContext(conf);

        List&amp;lt;SerializableClass&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; 5; i++) {
            list.add(new SerializableClass(i));
        }

        JavaRDD&amp;lt;SerializableClass&amp;gt; rdd = sc.parallelize(list);
        System.out.println(rdd.count()); // 직렬화 오류 없이 실행됨

        sc.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 직렬화가 가능하게 Serializable 인터페이스를 사용해야하며, 별도의 관리가 필요한 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이, python으로 재현하고자 하였는데 python에서는 위와 같은 재현이 어렵습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class NonSerializableClass:
    def __init__(self, value):
        self.value = value

# 사용자 정의 클래스의 인스턴스 생성
non_serializable_objects = [NonSerializableClass(i) for i in range(5)]

# RDD 생성 시도
rdd = spark.sparkContext.parallelize(non_serializable_objects)

# 액션을 통해 직렬화 강제 시도
rdd.collect()

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에는 자바와 비슷한 방식으로 오류가 발생할 것 같아 보이는 형식으로 재현한 코드이지만, 실제로는 정상작동하는 코드입니다. 그러한 이유는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 내장 직렬화 방식 (피클링 )&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python에서는 기본적으로 직렬화 및 역직렬화가 가능합니다&lt;/li&gt;
&lt;li&gt;즉, Java에서 Serializable 를 implements 해야 직렬화 되던 것이, 파이썬에서는 모든 Class에 구현 되어 있다라고 볼 수 있을 것 같음.&lt;/li&gt;
&lt;li&gt;KryoSerializer를 설정하더라도 크게 오류가 발생하지 않음.&lt;/li&gt;
&lt;li&gt;결론은 Python으로 작성한 Spark 코드에서는 큰 오류 없이 사용 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 현재 진행 중인 프로젝트에서 kryo 직렬화 방식을 추가해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존코드&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;def create_spark_session():
    return SparkSession.builder \\
        .appName(&quot;DataProcessing&quot;) \\
        .config(&quot;spark.hadoop.fs.s3a.endpoint&quot;, &quot;&amp;lt;https://s3.ap-northeast-2.amazonaws.com&amp;gt;&quot;) \\
        .config('spark.hadoop.fs.s3a.impl', 'org.apache.hadoop.fs.s3a.S3AFileSystem') \\
        .config('spark.hadoop.fs.s3a.aws.credentials.provider', 'org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider') \\
        .getOrCreate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정코드&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;def create_spark_session():
    return SparkSession.builder \\
        .appName(&quot;DataProcessing&quot;) \\
        .config(&quot;spark.hadoop.fs.s3a.endpoint&quot;, &quot;&amp;lt;https://s3.ap-northeast-2.amazonaws.com&amp;gt;&quot;) \\
        .config(&quot;spark.serializer&quot;, &quot;org.apache.spark.serializer.KryoSerializer&quot;) \\
        .config(&quot;spark.kryo.registrationRequired&quot;, &quot;true&quot;) \\
        .config('spark.hadoop.fs.s3a.impl', 'org.apache.hadoop.fs.s3a.S3AFileSystem') \\
        .config('spark.hadoop.fs.s3a.aws.credentials.provider', 'org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider') \\
        .getOrCreate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7BdSP/btsF92ddccb/moLSVbYqV0gJfwmVg8TkyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7BdSP/btsF92ddccb/moLSVbYqV0gJfwmVg8TkyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7BdSP/btsF92ddccb/moLSVbYqV0gJfwmVg8TkyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7BdSP%2FbtsF92ddccb%2FmoLSVbYqV0gJfwmVg8TkyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;751&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8초가 걸린 프로세스가 수정 전 코드이고, 7.4초가 걸린 코드는 kryo 직렬화를 적용한 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 성능개선이 이뤄진 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@charchitpatidar/data-serialization-an-optimization-technique-in-apache-spark-22f070628f6c&quot;&gt;https://medium.com/@charchitpatidar/data-serialization-an-optimization-technique-in-apache-spark-22f070628f6c&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@sathamn.n/serialization-and-deserialization-in-spark-a-quick-overview-34ef74b7471e&quot;&gt;https://medium.com/@sathamn.n/serialization-and-deserialization-in-spark-a-quick-overview-34ef74b7471e&lt;/a&gt;&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/35</guid>
      <comments>https://park-dev-diary.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 29 Mar 2024 13:39:16 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] Driud에 적재하는 task 작성하기</title>
      <link>https://park-dev-diary.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Druid에 데이터를 적재하기 이전에 S3를 medallion structure 활용하여 작업하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 중에 골드데이터를 기준으로 드루이드에 데이터를 적재하는 과정을 진행해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 대략적으로 정리해보면 medallion structure 이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2331&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8tYz/btsC6wgiCRl/jbAWsP7EAXfO6yV0CtpvWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8tYz/btsC6wgiCRl/jbAWsP7EAXfO6yV0CtpvWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8tYz/btsC6wgiCRl/jbAWsP7EAXfO6yV0CtpvWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8tYz%2FbtsC6wgiCRl%2FjbAWsP7EAXfO6yV0CtpvWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2331&quot; height=&quot;1000&quot; data-origin-width=&quot;2331&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 Druid는 local 버전으로 진행했음을 먼저 선언하고 간다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Druid에서 S3를 연결이 되는지를 확인한다. 우선 연결이 정상적으로 이뤄진다면 드루이드와 S3의 연결에는 문제가 없음을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 연결하면서 S3 데이터에 대한 Spec JSON 를 추출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 JSON 같은 경우에는 아래의 과정을 진행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1339&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7ueuA/btsC9bQbo6a/kIHfLhfs8MR09P5RKC7lck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7ueuA/btsC9bQbo6a/kIHfLhfs8MR09P5RKC7lck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7ueuA/btsC9bQbo6a/kIHfLhfs8MR09P5RKC7lck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7ueuA%2FbtsC9bQbo6a%2FkIHfLhfs8MR09P5RKC7lck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1339&quot; height=&quot;1000&quot; data-origin-width=&quot;1339&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 처음부터 S3에 대한 스펙을 작성하기 보단, 위와 같은 방법으로 스켈레톤 JSON를 활용하면 더 빠르게 작업할 수 있을 것 같다고 생각한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 Druid 작업 스펙을 작성하는 방법인데 이 부분은 Druid Document를 참고하면 더 자세한 정보를 얻을 수 있다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성한 Spec JSON에 대해서만 짤막하게 낙서한 부분이 있어 첨부하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUmAMI/btsC9BHQAFC/wz9HFullAfx3o2eAIvkjkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUmAMI/btsC9BHQAFC/wz9HFullAfx3o2eAIvkjkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUmAMI/btsC9BHQAFC/wz9HFullAfx3o2eAIvkjkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUmAMI%2FbtsC9BHQAFC%2Fwz9HFullAfx3o2eAIvkjkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;999&quot; height=&quot;1148&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 자신의 Spec JSON를 정의하게 되면 Airflow를 활용하여 드루이드에 적재할 수 있게 Task를 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나같은 경우에는 Airflow에서 지원하는 Druid Provider를 사용하다가 잘 안되는 부분들이 있었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분을 직접 해당 Druid API 를 활용해 Druid 에게 나의 작업스펙를 작성하여 Submit 하는 방향으로 작업하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704546359337&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from datetime import datetime
import requests,json


def post_to_druid():
    # Druid 적재 작업 명세(JSON)의 위치
    spec = {
    &quot;type&quot;: &quot;index_parallel&quot;,
    &quot;spec&quot;: {
      &quot;ioConfig&quot;: {
        &quot;type&quot;: &quot;index_parallel&quot;,
        &quot;inputSource&quot;: {
          &quot;type&quot;: &quot;s3&quot;,
          &quot;prefixes&quot;: [
            &quot;s3://data-lake/gold/eventsim&quot;
          ],
          &quot;filter&quot;: &quot;*.parquet&quot;,
          &quot;properties&quot;: {
            &quot;accessKeyId&quot;: {
              &quot;type&quot;: &quot;default&quot;,
              &quot;password&quot;: &quot;&quot;
            },
            &quot;secretAccessKey&quot;: {
              &quot;type&quot;: &quot;default&quot;,
              &quot;password&quot;: &quot;&quot;
            }
          }
        },
        &quot;inputFormat&quot;: {
          &quot;type&quot;: &quot;parquet&quot;
        }
      },
      &quot;tuningConfig&quot;: {
        &quot;type&quot;: &quot;index_parallel&quot;
      },
      &quot;dataSchema&quot;: {
        &quot;dataSource&quot;: &quot;eventsim&quot;,
        &quot;granularitySpec&quot;: {
          &quot;type&quot;: &quot;uniform&quot;,
          &quot;segmentGranularity&quot;: &quot;DAY&quot;,
          &quot;queryGranularity&quot;: &quot;HOUR&quot;,
          &quot;rollup&quot;: False
        },
        &quot;timestampSpec&quot;: {
          &quot;column&quot;: &quot;window.start&quot;,
          &quot;format&quot;: &quot;auto&quot;
        },
        &quot;dimensionsSpec&quot;: {
          &quot;dimensions&quot;: [
            &quot;window.end&quot;,
            &quot;free_count&quot;,
            &quot;paid_count&quot;,
            &quot;female_count&quot;,
            &quot;male_count&quot;
          ]
        },
        &quot;metricsSpec&quot;: [
          {
            &quot;type&quot;: &quot;count&quot;,
            &quot;name&quot;: &quot;count&quot;
          },
         {
            &quot;type&quot;: &quot;longSum&quot;,
            &quot;name&quot;: &quot;free_count_sum&quot;,
            &quot;fieldName&quot;: &quot;free_count&quot;
        },
        {
            &quot;type&quot;: &quot;longSum&quot;,
            &quot;name&quot;: &quot;paid_count_sum&quot;,
            &quot;fieldName&quot;: &quot;paid_count&quot;
        }
        ]
      }
    }
  }


    # Druid 적재 API 엔드포인트
    druid_url = &quot;http://192.168.1.229:8081/druid/indexer/v1/task&quot;

    headers = {
            'Content-Type': 'application/json'
            }

    spec = json.dumps( spec )

    try:
        # HTTP POST 요청으로 Druid에 작업 명세 전송
        response = requests.post(druid_url,headers=headers, data=spec)
        response.raise_for_status()  # 오류 발생 시 예외 발생

    except requests.exceptions.HTTPError as http_err:
        print(f&quot;HTTP 오류 발생: {http_err}&quot;)
        print(response.text)
    except Exception as err:
        print(f&quot;오류 발생: {err}&quot;)

default_args = {
    'owner': 'airflow',
    'start_date': datetime(2023, 1, 1),
    'retries': 1
}

with DAG('druid_s3_csv_ingestion', default_args=default_args, schedule_interval=None) as dag:
    druid_task = PythonOperator(
        task_id='post_to_druid',
        python_callable=post_to_druid
    )

    druid_task&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 druid TASK를 작성하여 Airflow에서 실행하였고, 실제로 정상적으로 Druid에 잘 적재되는 것을 확인하였다..&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/34</guid>
      <comments>https://park-dev-diary.tistory.com/34#entry34comment</comments>
      <pubDate>Sat, 6 Jan 2024 22:06:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spark] DockerCompose Airflow, PySpark DAG 실행하기</title>
      <link>https://park-dev-diary.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;내가 하고자 했던 것 =&amp;gt; Docker Compose로 Airflow 구성 후에 Spark DAG를 생성하여 실행시키기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였고, 그 과정 속에서 상당히 많은 부분들을 추가하고 삽질이 필요하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 과정 속에서 발생했던 트러블 중에 Airflow에서 Spark-submit이 정상적으로 전달이 되었지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TaskSetManager 에서 무한대기 상태로 빠져버린다는 것이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 Airflow에 java 추가하기, Spark master, Worker 컨테이너 DockerFile 만드는 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 여러 문제점들을 겪게 되었고, 해결하게 되는 과정들을 쭉 적어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Connection&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/apache/airflow/blob/89aa437ef98bba97ba14245d4ea3b61c443b2a25/docs/apache-airflow/howto/docker-compose/docker-compose.yaml#L4&quot;&gt;https://github.com/apache/airflow/blob/89aa437ef98bba97ba14245d4ea3b61c443b2a25/docs/apache-airflow/howto/docker-compose/docker-compose.yaml#L4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;airflow 프로젝트는 공식 Git Repe를 참고하여 만들었다. 그 중에 Connection를 추가하기 위해서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Provider에 대한 의존성을 추가해야한다. Docker-compose.yaml에 일 부분을 수정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:- apache-airflow-providers-postgres apache-airflow-providers-apache-spark}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에서 내려받은 compose.yaml에 해당 파라미터를 찾아 apache-spark 추가 된 부분을 넣어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가가 완료되면, airflow web UI에서 Admin &amp;gt; Connections &amp;gt; [+] 버튼을 눌러서 Spark Master와의 Connection를 추가해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmxH6R/btsCEJNiRIR/3OcgAgHTKfMW3uWvkVjrfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmxH6R/btsCEJNiRIR/3OcgAgHTKfMW3uWvkVjrfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmxH6R/btsCEJNiRIR/3OcgAgHTKfMW3uWvkVjrfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmxH6R%2FbtsCEJNiRIR%2F3OcgAgHTKfMW3uWvkVjrfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;276&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose로 Local에서 실행환경 기준으로 거의 위와 같이 Host,Port를 넣어주면 되는 것으로 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection ID는 Spark Operation의 파라미터에 필요한 값이니 기억할 필요가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Airflow Java 추가 ,Spark Worker,Master 도커 컴포즈에 추가하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 언급하였듯, Airflow 공식 도커 컴포즈 파일을 활용하였다. 그리고 이 부분에서 적절한 커스텀마이징이 필요하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Spark를 활용하기 위해서 수정이 필수였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 sparkOperation 이 명령어를 하기 위해선 Java를 요구하였기에 Airflow를 위한 Dockerfile를 따로 하나 만들어줘야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMpGvQ/btsCy6bKMqH/VryhiwrOeXJjVkqlxsmSvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMpGvQ/btsCy6bKMqH/VryhiwrOeXJjVkqlxsmSvK/img.png&quot; data-alt=&quot;수정 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMpGvQ/btsCy6bKMqH/VryhiwrOeXJjVkqlxsmSvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMpGvQ%2FbtsCy6bKMqH%2FVryhiwrOeXJjVkqlxsmSvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;219&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수정 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 수정 전의 airflow Docker Compose File이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 image 외에도 해당 컨테이너에 Java를 설치하고 환경변수를 추가해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sFusL/btsCymF5PiU/pkjw1i9aWx1c8xEIxdcPhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sFusL/btsCymF5PiU/pkjw1i9aWx1c8xEIxdcPhK/img.png&quot; data-alt=&quot;Airflow DockerFile&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sFusL/btsCymF5PiU/pkjw1i9aWx1c8xEIxdcPhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsFusL%2FbtsCymF5PiU%2Fpkjw1i9aWx1c8xEIxdcPhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;271&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Airflow DockerFile&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 DockerFIle를 작성한 후에&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwrDCQ/btsCIHhbCFO/2EfMEF4RzAEF4upjTx5D3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwrDCQ/btsCIHhbCFO/2EfMEF4RzAEF4upjTx5D3K/img.png&quot; data-alt=&quot; 수정 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwrDCQ/btsCIHhbCFO/2EfMEF4RzAEF4upjTx5D3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwrDCQ%2FbtsCIHhbCFO%2F2EfMEF4RzAEF4upjTx5D3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;196&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 수정 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 docker-compose.yaml 를 수정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 추가적으로 service에 Spark 컨테이너들을 구성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 코드는 Git에 올려놓을 예정이니, 그 부분을 참고해보면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 위에 추가해준 사항들을 제외하고도, Spark를 컨테이너에 대한 구성하고 추가하는 부분에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당한 애를 먹었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종버전 이전에는 bitnami/spark의 컨테이너를 그대로 활용해서 worker,master 컨테이너를 만들었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 master 까지 submit이 이뤄지는 것으로 Log가 체크가 되는데 특정부분에서 무한대기 상태로 빠지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에 대한 점검 혹은 원인파악으로부터 너무 많은 시간이 소요되었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국엔 명확한 원인파악이 되지 않았고, 추측되는 부분은 차용하고 있는 Spark image 컨테이너에 이슈가 있는 것으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추측하였고 그로인해 DockerFile로 따로 작성하여 Spark 컨테이너를 구축했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 잘 해결되었지만 다른 문제들이 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. TaskSetManager 이후에 무한대기 현상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정까지 전반적으로 Spark의 구성이 끝났고 이후에 DAG를 실행하여 결과를 지켜보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20분이 지나도 작업이 끝나지 않아 확인해보니&lt;/p&gt;
&lt;pre id=&quot;code_1703510831400&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[2023-12-25T06:39:10.508+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:10 INFO TaskSchedulerImpl: Adding task set 0.0 with 4 tasks resource profile 0

[2023-12-25T06:39:12.118+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO StandaloneSchedulerBackend$StandaloneDriverEndpoint: Registered executor NettyRpcEndpointRef(spark-client://Executor) (172.18.0.6:49528) with ID 0, ResourceProfileId 0

[2023-12-25T06:39:12.167+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO StandaloneSchedulerBackend$StandaloneDriverEndpoint: Registered executor NettyRpcEndpointRef(spark-client://Executor) (172.18.0.6:49530) with ID 1, ResourceProfileId 0

[2023-12-25T06:39:12.200+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO BlockManagerMasterEndpoint: Registering block manager 172.18.0.6:39665 with 434.4 MiB RAM, BlockManagerId(0, 172.18.0.6, 39665, None)

[2023-12-25T06:39:12.244+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO BlockManagerMasterEndpoint: Registering block manager 172.18.0.6:42191 with 434.4 MiB RAM, BlockManagerId(1, 172.18.0.6, 42191, None)

[2023-12-25T06:39:12.247+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO TaskSetManager: Starting task 0.0 in stage 0.0 (TID 0) (172.18.0.6, executor 0, partition 0, PROCESS_LOCAL, 7730 bytes)

[2023-12-25T06:39:12.273+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:12 INFO TaskSetManager: Starting task 1.0 in stage 0.0 (TID 1) (172.18.0.6, executor 1, partition 1, PROCESS_LOCAL, 7763 bytes)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 있는 로그에서 다음 프로세스로 넘어가지 않는 상황이 발생하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 PySpark의 구조 상 TaskSetManager가 속한 부분을 도식화 해보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1913&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7QwJy/btsCDe7HFtE/V5aANdhjTA0AQLV2thBL71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7QwJy/btsCDe7HFtE/V5aANdhjTA0AQLV2thBL71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7QwJy/btsCDe7HFtE/V5aANdhjTA0AQLV2thBL71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7QwJy%2FbtsCDe7HFtE%2FV5aANdhjTA0AQLV2thBL71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;552&quot; height=&quot;289&quot; data-origin-width=&quot;1913&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파란색 부분, 즉 Master 부분에서 더 이상 프로세스가 진행되고 있지 않는 상황이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분을 모니터링 혹은 로깅을 하고 싶었지만 spark image로만 구성해놓은 상황이라 자세한 로깅 자체가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽질 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위에 있는 로그로만 가정해보기론 Spark 컨테이너 내부적인 Setting 혹은 init 자체에 대한 부분이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제라고 생각했고, Git에 docker-compose로 작성한 spark 코드들을 찾아보면서 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 후 다시 DAG를 실행했을 때에도 동일한 문제가 발생하였다. 좌절할 뻔 했다. 해당 Log를 꼼꼼히 살펴보다 이상한 부분을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;​&lt;/p&gt;
&lt;pre id=&quot;code_1703510783305&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[2023-12-25T06:39:08.421+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO SparkContext: Running Spark version 3.5.0
​
[2023-12-25T06:39:08.423+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO SparkContext: OS info Linux, 5.10.124-linuxkit, aarch64
​
[2023-12-25T06:39:08.424+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO SparkContext: Java version 11.0.21
​
[2023-12-25T06:39:08.474+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
​
[2023-12-25T06:39:08.531+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO ResourceUtils: ==============================================================
​
[2023-12-25T06:39:08.532+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO ResourceUtils: No custom resources configured for spark.driver.
​
[2023-12-25T06:39:08.533+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO ResourceUtils: ==============================================================
​
[2023-12-25T06:39:08.534+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO SparkContext: Submitted application: SparkTest
​
[2023-12-25T06:39:08.535+0000] {spark_submit.py:521} INFO - 23/12/25 06:39:08 INFO SparkContext: Spark configuration:
​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;내가 Setting한 컨테이너에 Spark 버전은 3.4.2 이 였는데, SparkContext에서는 3.5.0으로 Running 중이라고 나온다.&lt;br /&gt;위에 있는 상황에서 추측을 해보았다.&lt;br /&gt;apache-airflow-providers-apache-spark 에서 이미 Spark 관련 버전이 Setting 되어있어 Submit 할 때&lt;br /&gt;자동으로 Spark 버전을 맞춰서 보내는 것 같다.&lt;br /&gt;위 추측으로 이것이 팩트인지를 찾기 위해 pyspark, airflow provider 관련 Document 를 찾아보기 시작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIXKPN/btsCy7aDPLL/kwEVxsBGgIbCmTYkrbRipk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIXKPN/btsCy7aDPLL/kwEVxsBGgIbCmTYkrbRipk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIXKPN/btsCy7aDPLL/kwEVxsBGgIbCmTYkrbRipk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIXKPN%2FbtsCy7aDPLL%2FkwEVxsBGgIbCmTYkrbRipk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;457&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위에는 airflow provider 를 넣을 때, pip install 되면 의존되는 것들 중에 Pyspark가 존재한다.&lt;br /&gt;즉 provider에 spark를 넣으면, pyspark가 자동으로 설치된다라는 소리이다.&lt;br /&gt;그리고 해당 버전에 대해서는 크게 언급되어 있지 않을 것을 보니, latest 한 버전이 설치되는 것이고&lt;br /&gt;그래서 자동으로 3.5.0 으로 설치됨을 추측해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyspark 와 spark의 Version은 동일하다고 Document에서 찾아볼 수있 었고&lt;br /&gt;그래서 위와 같이 버전을 spark 컨테이너 버전을 맞춰주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 Spark의 DAG를 Sucess 할 수 있었다.&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/33</guid>
      <comments>https://park-dev-diary.tistory.com/33#entry33comment</comments>
      <pubDate>Mon, 25 Dec 2023 22:32:46 +0900</pubDate>
    </item>
    <item>
      <title>[네이버 클라우드] Local에서 pyspark로 S3 읽고 저장하기</title>
      <link>https://park-dev-diary.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local에서 pyspark로 CSV를 읽고, 해당 CSV를 parquet으로 쪼개서 저장해야하는 작업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 AWS가 아닌 Naver Cloud에서 제공해주는 Object Storage 를 활용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 참고로 작업환경은 Mac M1 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Naver Cloud에서는 AWS형태의 모듈들을 전체적으로 지원이 되어서, 기존에 사용하던&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에 대한 권한 설정 혹은 세팅들을 전부 지원이 되어서 해당 모듈들을 활용하여&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Naver Cloud의 S3를 직접적으로 읽고 쓸 수 있는 편리함을 가지고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark로 S3 관련 모듈로 통해 AWS S3를 읽고 쓰는 코드들을 대체로 활용하면 Naver Cloud의 S3도 동일한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업들을 할 수 있었는데, 구글링에 pyspark aws s3 로 검색되는 코드들을 레퍼런스 삼아 작업도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 네이버 클라우드로는 레퍼런스 코드가 거의 없음 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링해서 찾은 결과,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1702889326292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql import SparkSession
from pyspark.sql.functions import col, to_date
from pyspark.sql.types import TimestampType

# AWS 자격 증명 설정
access_key = ''
secret_key = ''
endpoint_url = &quot;https://kr.object.ncloudstorage.com&quot;

# Spark 세션 초기화 및 S3 설정 추가
spark = SparkSession.builder.appName(&quot;csvToParquet&quot;) \
    .config(&quot;spark.jars.packages&quot;, &quot;org.apache.hadoop:hadoop-aws:3.2.0,com.amazonaws:aws-java-sdk-bundle:1.11.375&quot;) \
    .config(&quot;spark.hadoop.fs.s3a.access.key&quot;, access_key) \
    .config(&quot;spark.hadoop.fs.s3a.secret.key&quot;, secret_key) \
    .config(&quot;spark.hadoop.fs.s3a.endpoint&quot;, endpoint_url) \
    .getOrCreate()

# S3에서 CSV 파일 읽기
csv_file_path = &quot;s3a://data-lake/bronze/eventsim_big.csv&quot;
df = spark.read.csv(csv_file_path, header=True, inferSchema=True)

# 'ts' 필드를 날짜 형식으로 변환
df = df.withColumn(&quot;date&quot;, to_date((col(&quot;ts&quot;) / 1000).cast(TimestampType())))

# 날짜별로 데이터를 분할하고 Parquet으로 저장
output_base_path = &quot;s3a://data-lake/silver/eventsim/&quot;
df.write.partitionBy(&quot;date&quot;).parquet(output_base_path)

# Spark 세션 종료
spark.stop()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드로 실행을 시도하였고, 실행이 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생한 에러코드로는 아래와 같았다.&lt;/p&gt;
&lt;pre id=&quot;code_1702890485956&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Traceback (most recent call last):
  File &quot;/Users/parkjeongmin/workspace/event-project/naver_spark_s3.py&quot;, line 40, in &amp;lt;module&amp;gt;
    df = spark.read.csv(csv_file_path, header=True, inferSchema=True)
  File &quot;/Users/parkjeongmin/workspace/event-project/venv/lib/python3.9/site-packages/pyspark/sql/readwriter.py&quot;, line 740, in csv
    return self._df(self._jreader.csv(self._spark._sc._jvm.PythonUtils.toSeq(path)))
  File &quot;/Users/parkjeongmin/workspace/event-project/venv/lib/python3.9/site-packages/py4j/java_gateway.py&quot;, line 1322, in __call__
    return_value = get_return_value(
  File &quot;/Users/parkjeongmin/workspace/event-project/venv/lib/python3.9/site-packages/pyspark/errors/exceptions/captured.py&quot;, line 179, in deco
    return f(*a, **kw)
  File &quot;/Users/parkjeongmin/workspace/event-project/venv/lib/python3.9/site-packages/py4j/protocol.py&quot;, line 326, in get_return_value
    raise Py4JJavaError(
py4j.protocol.Py4JJavaError: An error occurred while calling o40.csv.
: java.lang.NoClassDefFoundError: com/amazonaws/auth/AWSCredentialsProvider
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:348)
        at org.apache.hadoop.conf.Configuration.getClassByNameOrNull(Configuration.java:2625)
        at org.apache.hadoop.conf.Configuration.getClassByName(Configuration.java:2590)
        at org.apache.hadoop.conf.Configuration.getClass(Configuration.java:2686)
        at org.apache.hadoop.fs.FileSystem.getFileSystemClass(FileSystem.java:3431)
        at org.apache.hadoop.fs.FileSystem.createFileSystem(FileSystem.java:3466)
        at org.apache.hadoop.fs.FileSystem.access$300(FileSystem.java:174)
        at org.apache.hadoop.fs.FileSystem$Cache.getInternal(FileSystem.java:3574)
        at org.apache.hadoop.fs.FileSystem$Cache.get(FileSystem.java:3521)
        at org.apache.hadoop.fs.FileSystem.get(FileSystem.java:540)
        at org.apache.hadoop.fs.Path.getFileSystem(Path.java:365)
        at org.apache.spark.sql.execution.streaming.FileStreamSink$.hasMetadata(FileStreamSink.scala:53)
        at org.apache.spark.sql.execution.datasources.DataSource.resolveRelation(DataSource.scala:366)
        at org.apache.spark.sql.DataFrameReader.loadV1Source(DataFrameReader.scala:229)
        at org.apache.spark.sql.DataFrameReader.$anonfun$load$2(DataFrameReader.scala:211)
        at scala.Option.getOrElse(Option.scala:189)
        at org.apache.spark.sql.DataFrameReader.load(DataFrameReader.scala:211)
        at org.apache.spark.sql.DataFrameReader.csv(DataFrameReader.scala:538)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at py4j.reflection.MethodInvoker.invoke(MethodInvoker.java:244)
        at py4j.reflection.ReflectionEngine.invoke(ReflectionEngine.java:374)
        at py4j.Gateway.invoke(Gateway.java:282)
        at py4j.commands.AbstractCommand.invokeMethod(AbstractCommand.java:132)
        at py4j.commands.CallCommand.execute(CallCommand.java:79)
        at py4j.ClientServerConnection.waitForCommands(ClientServerConnection.java:182)
        at py4j.ClientServerConnection.run(ClientServerConnection.java:106)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassNotFoundException: com.amazonaws.auth.AWSCredentialsProvider
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
        ... 31 more&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에러코드로만 추측해보았을 떈, AWSCredentialsProvider를 보고 AWS에 추가적인 권한설정 영역이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 구글링으로 서칭해봤을 때, 나와 동일한 오류로 질문한 stack overflow를 보게되었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1702890708387&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Configuring Pyspark with Amazon S3 giving java.lang.ClassNotFoundException: com.amazonaws.auth.AWSCredentialsProvider&quot; data-og-description=&quot;I'm trying to configure pyspark with Amazon s3 but got the following error: py4j.protocol.Py4JJavaError: An error occurred while calling o66.csv. : java.lang.NoClassDefFoundError: com/amazonaws/auth/&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&quot; data-og-url=&quot;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yA7GW/hyUPMLBYKQ/tJng6EhFwKwzrsjlhn06xk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/74647110/configuring-pyspark-with-amazon-s3-giving-java-lang-classnotfoundexception-com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yA7GW/hyUPMLBYKQ/tJng6EhFwKwzrsjlhn06xk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Configuring Pyspark with Amazon S3 giving java.lang.ClassNotFoundException: com.amazonaws.auth.AWSCredentialsProvider&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I'm trying to configure pyspark with Amazon s3 but got the following error: py4j.protocol.Py4JJavaError: An error occurred while calling o66.csv. : java.lang.NoClassDefFoundError: com/amazonaws/auth/&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기의 답변으로는 버전에 대한 문제와 설정에 대한 문제라고 지적해주고 있었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연스럽게 포커싱이 이러한 버전,의존성 문제로 삽질을 몇 시간동안 씨름하다가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메세지를 자세히 읽어보면, java.lang.ClassNotFoundException&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 자체를 못찾고 있었고, 상단에 config에 넣어주었던 aws-java-sdk.jar 가 정상적으로 의존되지 못함을&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추측하게 되었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;config(&quot;spark.jars.packages&quot;,&amp;nbsp;&quot;org.apache.hadoop:hadoop-aws:3.2.0,com.amazonaws:aws-java-sdk-bundle:1.11.375&quot;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 있는 설정 자체가 에러였음이라고 생각함..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 부분을 직접적으로 jars를 로컬에 받아놓고, 해당 부분을 직접적으로 File path를 잡아주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 실행이 되어버렸다. 몇 시간동안 잘못된 키워드로 삽질했고, 여러 &lt;a href=&quot;https://hadoop.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hadoop.apache.org/&lt;/a&gt;&amp;nbsp;도 보면서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인이 무엇인지도 모르다가, 뭔지 모르게 허무하게 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1702895991102&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql import SparkSession
from pyspark.sql.functions import col, to_date
from pyspark.sql.types import TimestampType

# AWS 자격 증명 설정
access_key = ''
secret_key = ''
endpoint_url = &quot;https://kr.object.ncloudstorage.com&quot;

spark = SparkSession.builder.appName(&quot;csvToParquet&quot;) \
    .config(&quot;spark.hadoop.fs.s3a.access.key&quot;, access_key) \
    .config(&quot;spark.hadoop.fs.s3a.secret.key&quot;, secret_key) \
    .config(&quot;spark.hadoop.fs.s3a.endpoint&quot;, endpoint_url) \
    .config(&quot;spark.driver.extraClassPath&quot;, &quot;/Users/parkjeongmin/workspace/event-project/aws-java-sdk-bundle-1.12.481.jar&quot;) \
    .config(&quot;spark.executor.extraClassPath&quot;, &quot;/Users/parkjeongmin/workspace/event-project/aws-java-sdk-bundle-1.12.481.jar&quot;) \
    .getOrCreate()


# S3에서 CSV 파일 읽기
csv_file_path = &quot;s3a://data-lake/bronze/eventsim.csv&quot;
df = spark.read.csv(csv_file_path, header=True, inferSchema=True)

# 'ts' 필드를 날짜 형식으로 변환
df = df.withColumn(&quot;date&quot;, to_date((col(&quot;ts&quot;) / 1000).cast(TimestampType())))

# 날짜별로 데이터를 분할하고 Parquet으로 저장
output_base_path = &quot;s3a://data-lake/silver/eventsim/&quot;
df.write.partitionBy(&quot;date&quot;).parquet(output_base_path)

# Spark 세션 종료
spark.stop()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 작업한 부분은 spark.driver.extraClassPath, spark.executor.extraClassPath 에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 mvn홈페이지에서 aws-java-sdk-bundle.jar를 설치한 경로를 바라보게 만들어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-bundle&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-bundle&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 작업이 필요한 영역은 구현이 되어서 다행이지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 오류를 보고 파악하는 부분에 대해 좀 더 확실하고 꼼꼼한 추론을 하였다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 많은 시간을 허비하지 않았을 것 같다. 특히, pyspark으로 s3 를 read하는 레퍼런스들 중에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고했을 때, 원하는 결과가 나오지 않았고, 키워드 위주로 검색하여 나온 몇 안되는 답변글에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집중하게 되니, 실제로 나에게 부합하지 않은 답변글임에도 그에 대한 판단을 못하고 시야가 좁아진 상태에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서칭을 하다보니 시간이 더욱 길어진 것 같다. 이런 부분을 개선할 수 있는 부분들이 뭐가 있을지, 또 이러한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황이 발생했을 때 지금 이 순간을 참고해볼 수 있음 좋겠다.&lt;/p&gt;</description>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/32</guid>
      <comments>https://park-dev-diary.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 18 Dec 2023 19:50:32 +0900</pubDate>
    </item>
    <item>
      <title>드루이드(druid) 튜토리얼 및 스크립트 분석</title>
      <link>https://park-dev-diary.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 드루이드를 로컬에 설치해보았고, 이를 사용해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 가면 Quickstart Document에 어떻게 실행하는지 대략적으로 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://druid.apache.org/docs/latest/tutorials/&quot;&gt;https://druid.apache.org/docs/latest/tutorials/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Quickstart (local) | Apache&amp;reg; Druid&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;!--&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;druid.apache.org](&lt;a href=&quot;https://druid.apache.org/docs/latest/tutorials/&quot;&gt;https://druid.apache.org/docs/latest/tutorials/&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 동일하게 실행하게 되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼 테이블이 하나 만들어지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Quickstart 방식에서는 특정 폴더에 파일을 불러오면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일포멧팅에 맞게 테이블을 만들어 줄 수 있는 쿼리를 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 쿼리를 실행하면 드루이드 테이블을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법으로도 쉽게 만들 수 있는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 방법으로 작성하게 되면 Rollup 옵션이라던지, 필요한 디테일한 명령어들을 실행할 수 없는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://druid.apache.org/docs/latest/tutorials/tutorial-rollup&quot;&gt;https://druid.apache.org/docs/latest/tutorials/tutorial-rollup&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Aggregate data with rollup | Apache&amp;reg; Druid&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;!--&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;druid.apache.org](&lt;a href=&quot;https://druid.apache.org/docs/latest/tutorials/tutorial-rollup&quot;&gt;https://druid.apache.org/docs/latest/tutorials/tutorial-rollup&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 튜토리얼 페이지를 잘 읽어보면 드루이드의 테이블을 Ingestion 하는 스크립트를 작성하는 부분을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot; : &quot;index_parallel&quot;,
  &quot;spec&quot; : {
    &quot;dataSchema&quot; : {
      &quot;dataSource&quot; : &quot;rollup-tutorial&quot;,
      &quot;dimensionsSpec&quot; : {
        &quot;dimensions&quot; : [
          &quot;srcIP&quot;,
          &quot;dstIP&quot;
        ]
      },
      &quot;timestampSpec&quot;: {
        &quot;column&quot;: &quot;timestamp&quot;,
        &quot;format&quot;: &quot;iso&quot;
      },
      &quot;metricsSpec&quot; : [
        { &quot;type&quot; : &quot;count&quot;, &quot;name&quot; : &quot;count&quot; },
        { &quot;type&quot; : &quot;longSum&quot;, &quot;name&quot; : &quot;packets&quot;, &quot;fieldName&quot; : &quot;packets&quot; },
        { &quot;type&quot; : &quot;longSum&quot;, &quot;name&quot; : &quot;bytes&quot;, &quot;fieldName&quot; : &quot;bytes&quot; }
      ],
      &quot;granularitySpec&quot; : {
        &quot;type&quot; : &quot;uniform&quot;,
        &quot;segmentGranularity&quot; : &quot;week&quot;,
        &quot;queryGranularity&quot; : &quot;minute&quot;,
        &quot;intervals&quot; : [&quot;2018-01-01/2018-01-03&quot;],
        &quot;rollup&quot; : true
      }
    },
    &quot;ioConfig&quot; : {
      &quot;type&quot; : &quot;index_parallel&quot;,
      &quot;inputSource&quot; : {
        &quot;type&quot; : &quot;local&quot;,
        &quot;baseDir&quot; : &quot;quickstart/tutorial&quot;,
        &quot;filter&quot; : &quot;rollup-data.json&quot;
      },
      &quot;inputFormat&quot; : {
        &quot;type&quot; : &quot;json&quot;
      },
      &quot;appendToExisting&quot; : false
    },
    &quot;tuningConfig&quot; : {
      &quot;type&quot; : &quot;index_parallel&quot;,
      &quot;partitionsSpec&quot;: {
        &quot;type&quot;: &quot;dynamic&quot;
      },
      &quot;maxRowsInMemory&quot; : 25000
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;type&lt;/b&gt;: Ingestion 작업의 유형을 정의합니다. **index_parallel**은 병렬 처리를 지원하는 인덱싱 작업을 의미합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec&lt;/b&gt;: Ingestion 작업의 구체적인 사양을 정의하는 부분입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;dataSchema&lt;/b&gt;: 데이터 스키마에 대한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;dataSource&lt;/b&gt;: 적재할 데이터의 대상 데이터 소스(테이블)의 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;timestampSpec&lt;/b&gt;: 타임스탬프 데이터를 처리하는 방법을 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;column&lt;/b&gt;: 타임스탬프 값이 있는 컬럼의 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;format&lt;/b&gt;: 타임스탬프 데이터의 형식입니다. 여기서는 ISO 표준 형식을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dimensionsSpec&lt;/b&gt;: 차원(디멘션)에 대한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;dimensions&lt;/b&gt;: 차원으로 사용할 컬럼의 목록입니다. 예시에는 **srcIP**와 **dstIP**가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;metricsSpec&lt;/b&gt;: 메트릭(측정 가능한 데이터)에 대한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 메트릭 설정은 메트릭의 유형(&lt;b&gt;type&lt;/b&gt;), 이름(&lt;b&gt;name&lt;/b&gt;), 그리고 필드 이름(&lt;b&gt;fieldName&lt;/b&gt;)을 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;granularitySpec&lt;/b&gt;: 데이터의 세분성 및 롤업에 대한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;segmentGranularity&lt;/b&gt;: 데이터 세그먼트의 시간적 범위를 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;queryGranularity&lt;/b&gt;: 쿼리 시 데이터를 얼마나 세분화하여 처리할지 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;intervals&lt;/b&gt;: 처리할 시간 범위를 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rollup&lt;/b&gt;: 데이터 롤업(요약)을 활성화할지 여부를 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ioConfig&lt;/b&gt;: 입력 데이터 및 Ingestion 작업 유형에 관한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;inputSource&lt;/b&gt;: 데이터 소스에 대한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;type&lt;/b&gt;: 데이터 소스의 유형을 정의합니다. 여기서는 로컬 파일 시스템을 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;baseDir&lt;/b&gt; 및 &lt;b&gt;filter&lt;/b&gt;: 데이터 파일의 위치와 파일 이름(또는 패턴)을 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;inputFormat&lt;/b&gt;: 입력 데이터 형식을 정의합니다. 여기서는 JSON 형식을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;appendToExisting&lt;/b&gt;: 기존 데이터에 추가할지 여부를 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tuningConfig&lt;/b&gt;: Ingestion 작업의 성능 조정에 관한 설정입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;maxRowsPerSegment&lt;/b&gt;: 각 세그먼트 당 최대 행 수를 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;maxRowsInMemory&lt;/b&gt;: 메모리에 유지할 수 있는 최대 행 수를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 있는 Ingestion 스크립트에 대한 자세한 설명을 첨언했다.( GPT 참고 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 스크립트 기준으로 다양한 파일포멧들을 ingestion 스크립트를 작성해서 실행하면 적재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 위 스크립트를 잘 작성하는 것이 드루이드 테이블의 크기를 줄일 수 있고, 효과적인 쿼리실행 결과를 보장해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Rollup 이라는 키워드로 공식문서에서 개념을 설명해주는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 말하면, 저장하는 테이블에 카운트를 해줄 수 있는 매트릭을 만들어주고, 특정 중복되는 데이터들을 집계해주는 역활을 해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 rollup를 false를 하고 저장할 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Time/stamp/Page/VisitorID&lt;/p&gt;
&lt;table style=&quot;background-color: #343541; color: #d1d5db; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:00:00&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:00:00&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:00:00&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:01:00&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:01:00&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 저장이 된다면, rollup를 true로 인자값을 넣으면 아래와 같이 집계가 될 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Time/ stamp / Page / Count
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;2023-03-01 12:00:00&lt;/th&gt;
&lt;th&gt;Home&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:00:00&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-03-01 12:01:00&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 Rollup이라는 옵션과 디멘션, 그리고 그외 그렌티 Spec 들을 작성할 수 있는 요령이 드루이드를 효율적으로 쓰는 방향일 것이라는 생각이 든다.&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/31</guid>
      <comments>https://park-dev-diary.tistory.com/31#entry31comment</comments>
      <pubDate>Fri, 15 Dec 2023 23:24:46 +0900</pubDate>
    </item>
    <item>
      <title>M1으로 docker 로 설치하다 실패해 local로 설치하는 과정</title>
      <link>https://park-dev-diary.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;드루이드를 설치하면서 겪었던 경험&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;M1으로 설치하기엔 너무 많은 어려움이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 나의 경우, docker로 druid를 설치하여 사용하길 원하였고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해당 부분을 위해 druid 공식 Git에서 Docker 파일을 찾았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 진행해야 하는지에 대한 ReadMe가 적혀있었고, 가이드라인대로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/apache/druid/tree/master/distribution/docker&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/apache/druid/tree/master/distribution/docker&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 명시되어 있기론 maven를 빌드를 진행한 후에,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드된 파일을 기준으로 docker image를 진행하라고 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 문제.&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #161b22; color: #e6edf3; text-align: left;&quot;&gt;&lt;code&gt;mvn clean package -DskipTests -Pdist&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 실행하면 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생하는 문제는 bigdecimal 관련 설치되는 라이브러리가 문제인 것 같은데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;druid 관련 에러 검색키워드로 찾아보아도 나와 유사한 사람이 하나도 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 답답했는데, 구글에 대부분 하나정도 유사한 사례를 가지고 있는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문글 하나라도 보기 어려운 케이스는 처음인 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치하다 실패했고, 다시 설치해보고 실패하고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Mvc 빌드 부분 건너뛰고 도커 설치해보려고 하는데 그것도 정상적으로 안되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음.. 그래서 docker container로 작성된 yaml을 실행하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 설치도 정상적으로 되고, 도커에 정상적으로 올라갔고, localhost로 접속시 정상적인 대쉬보드가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 결정적으로는 정상적인 실행이 안되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런가 하고 보니, 실행되는 컨테이너들 중 꼭 하나가 죽어버리는 상황이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 자세히 보니, 드루이드에서 실행하는 각 컨테이너들의 메모리들 다 합쳐서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커의 RAM의 크기가 최소 7기가가 요구한다고 한다. 그래서 Docker 데스크탑에 가서 변경하는 부분에 가서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정해주었다. 물론 잘 되진 않았다. 그래서 여기서부터 멘붕이 살짝 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 HelloWorld 보기 어려웠던 경험은 오랜만인 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행되는 도커 컨테이너 CLI 접근해서 로그를 보니 OOM이 발생하고 있었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선 더 진행하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://druid.apache.org/docs/latest/tutorials/docker&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://druid.apache.org/docs/latest/tutorials/docker&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어보니 메모리에 대한 언급이 나와있다. 해당 도커 컴포즈에서는 7GB 가량 Ram 공간을 요구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 없으면 137 에러가 발생한다고 한다. 위에서도 언급했듯, 도커 데스크탑에서 용량 세팅을 하더라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제가 계속 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사항들을 쭉 겪어보고, 곰곰히 생각해서 성급한 결론은 M1 자체와 호환이 안되는 영역들과 그만한 성능을 받쳐주지 않으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 드루이드를 도커 컴포즈 형태로 실행하기 어렵다 라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 드루이드를 도커를 사용하지 않고, 로컬에서 싱글모드로 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://druid.apache.org/downloads/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://druid.apache.org/downloads/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1702560015185&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Download | Apache&amp;reg; Druid&quot; data-og-description=&quot;Download&quot; data-og-host=&quot;druid.apache.org&quot; data-og-source-url=&quot;https://druid.apache.org/downloads/&quot; data-og-url=&quot;https://druid.apache.org/downloads&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bx3wWC/hyUL5SNZhS/WP9OkXpnzMKBaJOb7yyuKK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380,https://scrap.kakaocdn.net/dn/dABUzt/hyULQ2rmmh/uuHRpoG4GcrID5gKutIMwK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380,https://scrap.kakaocdn.net/dn/F6d2a/hyULYMRM4i/OnTrGKDDNd7OdPoX74qIUK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380&quot;&gt;&lt;a href=&quot;https://druid.apache.org/downloads/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://druid.apache.org/downloads/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bx3wWC/hyUL5SNZhS/WP9OkXpnzMKBaJOb7yyuKK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380,https://scrap.kakaocdn.net/dn/dABUzt/hyULQ2rmmh/uuHRpoG4GcrID5gKutIMwK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380,https://scrap.kakaocdn.net/dn/F6d2a/hyULYMRM4i/OnTrGKDDNd7OdPoX74qIUK/img.png?width=1100&amp;amp;height=380&amp;amp;face=0_0_1100_380');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Download | Apache&amp;reg; Druid&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Download&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;druid.apache.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 페이지에 설치하는 방법은 잘 나와있다. 바이너리로 된 파일을 다운받고 알집을 푼 후에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;quickstart에 나와있는 가이드라인대로 바로 드루이드를 실행시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 몇 분만에 드루이드를 실행하였고, 도커 컴포즈에서 드루이드의 튜토리얼를 실행하면 Task가 맛탱이가 가던&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현상이 아닌, 정상적으로 드루이드에 테이블을 만들어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2178&quot; data-origin-height=&quot;1288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vljxZ/btsB3uiDq5F/2HMKqQTBbK5aKj31AC5iw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vljxZ/btsB3uiDq5F/2HMKqQTBbK5aKj31AC5iw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vljxZ/btsB3uiDq5F/2HMKqQTBbK5aKj31AC5iw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvljxZ%2FbtsB3uiDq5F%2F2HMKqQTBbK5aKj31AC5iw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;274&quot; data-origin-width=&quot;2178&quot; data-origin-height=&quot;1288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/30</guid>
      <comments>https://park-dev-diary.tistory.com/30#entry30comment</comments>
      <pubDate>Fri, 15 Dec 2023 10:15:33 +0900</pubDate>
    </item>
    <item>
      <title>Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(4)</title>
      <link>https://park-dev-diary.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 게시물에서 SuperSet를 활용해 시각화 할 수 있는 데이터들을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL로 마이그레이션하는 작업을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 데이터들을 기준으로 Superset를 활용해 차트를 만들고, 해당 차트들을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대쉬보드에 붙여넣어서 최종적으로 보기 좋은 대쉬보드를 구성하는 것이 중요한 목표인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나같은 경우 데이터를 시각화하는 방법에 대해 정확한 인사이트가 없어서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공해주는 Superset의 UI를 유추만으로 만들어서 굉장히 조약하게 만들 수밖에 없었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bex35Y/btsBBNKvMwc/JLRGX0AhmP3ebdS04JK950/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bex35Y/btsBBNKvMwc/JLRGX0AhmP3ebdS04JK950/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bex35Y/btsBBNKvMwc/JLRGX0AhmP3ebdS04JK950/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbex35Y%2FbtsBBNKvMwc%2FJLRGX0AhmP3ebdS04JK950%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2720&quot; height=&quot;1400&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성해보았는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월별 무료/유료 사용자 추세를 차트라인으로 만들었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도시별 사용자 통계는 도시별로 그룹바이를 하여, 총 사용자들을 카운트하여,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 미국 도시별 제공해주는 유니코드? 도시코드? 컬럼을 활용해 각각의 지역들을 맵핑해줄 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701955127127&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT CONCAT('US-', SUBSTRING(location, LOCATE(',', location) + 2)) AS `My column`,
       COUNT(*) AS count
FROM mydatabase.user_song_count
GROUP BY CONCAT('US-', SUBSTRING(location, LOCATE(',', location) + 2))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 기존 데이터가 저 지도의 특정코드와 매핑될 수 있게 수정해준 쿼리이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;1222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qeHxX/btsBCk8T1Vq/yIRTBkcoz7hViRWLHttT01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qeHxX/btsBCk8T1Vq/yIRTBkcoz7hViRWLHttT01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qeHxX/btsBCk8T1Vq/yIRTBkcoz7hViRWLHttT01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqeHxX%2FbtsBCk8T1Vq%2FyIRTBkcoz7hViRWLHttT01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;428&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;1222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 내가 주로 SuperSet를 활용하여 시각화하기 위해 주로 했던 작업은 우선 쿼리로 데이터를 추출한 다음,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 쿼리를 기준으로 데이터를 매핑해주는 작업을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 지도작업을 했을 때 설정했던 부분인데 따로 가이드라인이 없어도 유추해놓을 수 있게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI를 잘 만들어 놓은 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 도시별 사용자 통계 차트같은 경우 해당 테이블의 데이터 갯수가 천만개 정도 되었기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드되는데 굉장히 많은 시간이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이러한 부분들을 보충할 수 있게 한 작업들이 이전에서 했던 집계테이블을 만드는 작업이였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/28&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://park-dev-diary.tistory.com/28&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701955515519&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(3)&quot; data-og-description=&quot;https://park-dev-diary.tistory.com/26 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(2) https://park-dev-diary.tistory.com/25 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1) https://park-dev-diary.t&quot; data-og-host=&quot;park-dev-diary.tistory.com&quot; data-og-source-url=&quot;https://park-dev-diary.tistory.com/28&quot; data-og-url=&quot;https://park-dev-diary.tistory.com/28&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKFV0M/hyUIyAF3rY/xvi4McGDgSDR1zBrikfmp1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/byYsLo/hyUIwXbf4s/7dSElWH0eEfxkKaQtYd3l0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dvDtui/hyUIwCR1jc/Ix8i4lJ3TL5NYGp381z6T0/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/28&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://park-dev-diary.tistory.com/28&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKFV0M/hyUIyAF3rY/xvi4McGDgSDR1zBrikfmp1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/byYsLo/hyUIwXbf4s/7dSElWH0eEfxkKaQtYd3l0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dvDtui/hyUIwCR1jc/Ix8i4lJ3TL5NYGp381z6T0/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(3)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://park-dev-diary.tistory.com/26 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(2) https://park-dev-diary.tistory.com/25 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1) https://park-dev-diary.t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;park-dev-diary.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 작업을 통해 집계테이블을 만든 일별 음악들은 횟수, 월별 무료/유료 추세 차트는 로드되는데 최대 2초 정도 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 더 복잡한 대쉬보드였더라면, 더 많은 집계테이블과 복잡한 형태로 쪼개야만 최적화의 대쉬보드를 만들 수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;있을 것 같다라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 집계테이블을 설계 혹은 구안할 때, 최대한 많은 디멘션들을 포용할 수 있고 데이터량도 줄일 수 있는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 혹은 노하우들을 최대한 많이 쌓아놓는 것도 중요하다고 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 디멘션들이 여러개로 쪼개지고, 해당 디멘션들이 카디널리티가 높은 상태를 갖추고 있을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개의 집계테이블을 구상하여 최적화할 수 있는 방법도 있을 것이며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 ETL 작업에서 그렇게 쪼개진 여러 집계테이블들을 자동화하여, 여러번 거쳐야 하는 작업들을 최적화하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 시키는 것이 얼마나 중요한지를 이해할 수 있는 맥락인 것 같다고 생각했다.&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/29</guid>
      <comments>https://park-dev-diary.tistory.com/29#entry29comment</comments>
      <pubDate>Thu, 7 Dec 2023 22:30:02 +0900</pubDate>
    </item>
    <item>
      <title>Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(3)</title>
      <link>https://park-dev-diary.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/26&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://park-dev-diary.tistory.com/26&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701529244325&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(2)&quot; data-og-description=&quot;https://park-dev-diary.tistory.com/25 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1) https://park-dev-diary.tistory.com/21 Eventsim 빌드 및 실행하기 이 모든 과정은 MacOS 기준을 작성되었습니다! 1. JAVA &quot; data-og-host=&quot;park-dev-diary.tistory.com&quot; data-og-source-url=&quot;https://park-dev-diary.tistory.com/26&quot; data-og-url=&quot;https://park-dev-diary.tistory.com/26&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jLGLc/hyUIyGnXSS/GxKGQzsi0612ciPS1GhR3K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dEZ2xL/hyUIxnaZb7/HkOgcohX2vSqQRTsWPIE6K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/f1BkW/hyUIzywxd8/RqeuLVQ7tGyWcaVAmOkPWK/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/26&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://park-dev-diary.tistory.com/26&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jLGLc/hyUIyGnXSS/GxKGQzsi0612ciPS1GhR3K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dEZ2xL/hyUIxnaZb7/HkOgcohX2vSqQRTsWPIE6K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/f1BkW/hyUIzywxd8/RqeuLVQ7tGyWcaVAmOkPWK/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(2)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://park-dev-diary.tistory.com/25 Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1) https://park-dev-diary.tistory.com/21 Eventsim 빌드 및 실행하기 이 모든 과정은 MacOS 기준을 작성되었습니다! 1. JAVA&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;park-dev-diary.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이전에 Pandas로 진행하다가 안되었던 이유는 간단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #383a42; text-align: start;&quot;&gt;df = pd.read_json(file_path, lines=&lt;/span&gt;&lt;span style=&quot;color: #0184bb; text-align: start;&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #383a42; text-align: start;&quot;&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 주석처리하지 않고 그대로 작업을 해서 결국엔 chunk, ijson 둘다 실행하기 이전에 큰 데이터를 읽게 되어&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터져버리게 되었던 것이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석처리하고 실행하니, Pandas로도 Insert 작업엔 크게 무리가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그렇게 넣은 데이터들을 aggreate 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas, Spark 두 라이브러리를 활용하여 진행했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark 코드는 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701529746897&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, StringType, LongType, IntegerType, FloatType
from pyspark.sql.functions import col, from_unixtime, to_date, count
from pyspark.sql import functions as F
# Spark 세션 초기화
spark = SparkSession.builder \
    .appName(&quot;JsonToMySQLDailyAggregate&quot;) \
    .config(&quot;spark.jars&quot;, &quot;/Users/parkjeongmin/workspace/event-project/mysql-connector-java-8.0.28.jar&quot;) \
    .config(&quot;spark.driver.extraClassPath&quot;, &quot;/Users/parkjeongmin/workspace/event-project/mysql-connector-java-8.0.28.jar&quot;) \
    .getOrCreate()

# 스키마 정의
schema = StructType([
    StructField(&quot;ts&quot;, LongType(), True),
    StructField(&quot;userId&quot;, StringType(), True),
    StructField(&quot;sessionId&quot;, IntegerType(), True),
    StructField(&quot;page&quot;, StringType(), True),
    StructField(&quot;auth&quot;, StringType(), True),
    StructField(&quot;method&quot;, StringType(), True),
    StructField(&quot;status&quot;, IntegerType(), True),
    StructField(&quot;level&quot;, StringType(), True),
    StructField(&quot;itemInSession&quot;, IntegerType(), True),
    StructField(&quot;location&quot;, StringType(), True),
    StructField(&quot;userAgent&quot;, StringType(), True),
    StructField(&quot;lastName&quot;, StringType(), True),
    StructField(&quot;firstName&quot;, StringType(), True),
    StructField(&quot;registration&quot;, LongType(), True),
    StructField(&quot;gender&quot;, StringType(), True),
    StructField(&quot;tag&quot;, StringType(), True),
    StructField(&quot;artist&quot;, StringType(), True),
    StructField(&quot;song&quot;, StringType(), True),
    StructField(&quot;length&quot;, FloatType(), True)
])

# JSON 파일 읽기
df = spark.read.json(&quot;event.data.json&quot;, schema=schema)

# 타임스탬프를 날짜로 변환
df = df.withColumn(&quot;date&quot;, to_date(from_unixtime(col(&quot;ts&quot;) / 1000)))

# 일 단위로 집계
daily_agg_df = df.groupBy(&quot;date&quot;, &quot;level&quot;, &quot;location&quot;, &quot;gender&quot;).agg(
    F.countDistinct(&quot;userId&quot;).alias(&quot;unique_user_count&quot;),  # 고유한 사용자 수
    F.countDistinct(&quot;sessionId&quot;).alias(&quot;unique_session_count&quot;),  # 고유한 세션 수
    F.count(&quot;song&quot;).alias(&quot;total_song_plays&quot;),  # 총 노래 재생 횟수
    F.sum(&quot;length&quot;).alias(&quot;total_play_time&quot;)  # 총 재생 시간
)

# 데이터 MySQL에 저장
daily_agg_df.write \
    .format(&quot;jdbc&quot;) \
    .option(&quot;url&quot;, &quot;jdbc:mysql://localhost/mydatabase&quot;) \
    .option(&quot;dbtable&quot;, &quot;user_daily_song_count&quot;) \
    .option(&quot;user&quot;, &quot;root&quot;) \
    .option(&quot;password&quot;, &quot;my-secret-pw&quot;) \
    .mode(&quot;append&quot;) \
    .save()

# Spark 세션 종료
spark.stop()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark로 실행한 코드는 매우 정상적으로 잘 돌아갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 문제의 Pandas.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Pandas로 데이터들을 넣으려고 할 때, 읽는 데이터가 너무 큰 데이터라 터져버렸기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunksize를 줘서 조각내서 데이터를 읽고 agg 작업을 진행하였는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 코드는 이러하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701530069662&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aggregated_data = pd.DataFrame()

# JSON 파일을 청크 단위로 읽고 집계
for chunk in pd.read_json(file_path, lines=True, chunksize=chunk_size):
    chunk['date'] = pd.to_datetime(chunk['ts'], unit='ms').dt.date
    aggregated_chunk = chunk.groupby(['date', 'level', 'location', 'gender']).agg({
        'userId': pd.Series.nunique,
        'sessionId': pd.Series.nunique,
        'song': 'count',
        'length': 'sum'
    }).rename(columns={
        'userId': 'unique_user_count',
        'sessionId': 'unique_session_count',
        'song': 'total_song_plays',
        'length': 'total_play_time'
    }).reset_index()
    # aggregated_data.append(aggregated_chunk)

    aggregated_data = pd.concat([aggregated_data, aggregated_chunk], ignore_index=True)

# 모든 청크의 집계 결과를 하나의 DataFrame으로 합침
# final_aggregated_df = pd.concat(aggregated_data)

# 데이터베이스에 저장
try:
    aggregated_data.to_sql(con=database_connection, name='user_daily_song_count2', if_exists='append', index=False)
except Exception as e:
    print(f&quot;Error occurred: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 문제점은 각 청크데이터로만 agg을 시도하였고, 결국엔 조각조각 합계를 시도한 후에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 합쳐버려서 종합적으로는 조각된 합계들을 뭉쳐서 저장하게 되었던 것이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 정상적으로 잘 집계가 되면, 전체적인 데이터 수는 30만개 정도로 나오게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 Pandas 코드를 실행하게 되면, 300만개 정도의 데이터가 Insert 되는 현상이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 어떻게 집계해야하는지, Chunk된 조각데이터들을 합치기 위해서는 새로운 DataFrame를 만들고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk데이터를 지속적으로 UpSert 하는 느낌으로 진행되어야 할 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러 삽질을 하다가,&amp;nbsp; 모든 청크의 집계결과를 하나의 DataFrame으로 만들어 놓고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 DataFrame를 또 한번 집계를 수행하는 방식을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701530739929&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chunk_size = 1000

aggregated_chunks = []

# JSON 파일을 청크 단위로 읽고 집계
for chunk in pd.read_json(file_path, lines=True, chunksize=chunk_size):
    chunk['date'] = pd.to_datetime(chunk['ts'], unit='ms').dt.date
    aggregated_chunk = chunk.groupby(['date', 'level', 'location', 'gender']).agg({
        'userId': pd.Series.nunique,
        'sessionId': pd.Series.nunique,
        'song': 'count',
        'length': 'sum'
    }).rename(columns={
        'userId': 'unique_user_count',
        'sessionId': 'unique_session_count',
        'song': 'total_song_plays',
        'length': 'total_play_time'
    }).reset_index()
    aggregated_chunks.append(aggregated_chunk)

# 모든 청크의 집계 결과를 하나의 DataFrame으로 합침
final_aggregated_df = pd.concat(aggregated_chunks, ignore_index=True)

# 최종 집계 수행
final_aggregated_df = final_aggregated_df.groupby(['date', 'level', 'location', 'gender']).sum().reset_index()

# 데이터베이스에 저장
try:
    final_aggregated_df.to_sql(con=database_connection, name='user_daily_song_count2', if_exists='append', index=False)
except Exception as e:
    print(f&quot;Error occurred: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니, 정상적으로 데이터가 집계되어 들어가게 되었음을 확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위 비교군을 가지고 Pandas와 Spark의 성능을 비교할 수 있는 비교군 코드를 만들 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이렇게 만들어진 데이터를 가지고 시각화를 해보자.. 이젠..&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/28</guid>
      <comments>https://park-dev-diary.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 3 Dec 2023 00:29:02 +0900</pubDate>
    </item>
    <item>
      <title>[정글 PintOS] 1주차</title>
      <link>https://park-dev-diary.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상황:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;thread_create() 함수를 사용하여 쓰레드를 생성하고, 실행할 함수로 timer_sleep() 또는 이의 래퍼(wrapper) 함수를 전달한다.&lt;/li&gt;
&lt;li&gt;생성된 쓰레드가 실행되면서 설정한 시간이 경과했는지 확인한다.&lt;/li&gt;
&lt;li&gt;설정한 시간에 도달하지 않았다면, 해당 쓰레드를 ready_list에 추가한다.&lt;/li&gt;
&lt;li&gt;ready_list의 첫 번째 쓰레드를 실행한다.&lt;br /&gt;여러 쓰레드를 생성한 후에 2-3-4 과정을 반복하면서 각 쓰레드가 설정한 시간에 도달했는지 확인하고, 도달하면 해당 쓰레드를 종료한다. Test Case 결과에 나타나는 'kernel ticks', 'idle ticks', 'user ticks'는 다음과 같은 의미를 가진다:&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kernel ticks: 커널 쓰레드가 실행되는 데 걸리는 시간&lt;br /&gt;idle ticks: idle 쓰레드가 실행되는 데 걸리는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;user ticks: 사용자 프로그램이 실행되는 데 걸리는 시간&lt;br /&gt;Busy Waiting 방식을 사용하면, 모든 쓰레드가 종료될 때까지 idle 쓰레드는 실행되지 않는다. 따라서, 결과는 다음과 같을 수 있다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;idle ticks: 0&lt;br /&gt;kernel ticks: 800&lt;br /&gt;user ticks: 0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위 코드를 자세히 살펴보면 동작되는 구조는 Busy-waiting형태로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Busy-waiting형태란 프로그램이 다른 작업을 담당하며 기다리는 것이 아닌,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 조건이 만족하지 않는다면, 만족할 때 까지 계속 체크를 하면서 기다리는 작업을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위 코드는 BusyWaiting 방식을 Alarm Clock이라는 구조로 변경해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alram Clock이라는 구조는 프로그램이 시간이 지나면, 다음 작업을 수행하도록 예약을 하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템인데, 이렇게 구조를 변경하게 된다면 프로그램은 대기 상태에 들어가게 되면 CPU 사이클을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하지 않아 BusyWaiting에 비해 CPU자원을 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciM7Wg/btsA4enycik/kSqFpU0CWcKsscK48DcfiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciM7Wg/btsA4enycik/kSqFpU0CWcKsscK48DcfiK/img.png&quot; data-alt=&quot;devices/timer.c&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciM7Wg/btsA4enycik/kSqFpU0CWcKsscK48DcfiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciM7Wg%2FbtsA4enycik%2FkSqFpU0CWcKsscK48DcfiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1570&quot; height=&quot;352&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;devices/timer.c&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 기존 thread_yield() 호출 시 잠든 스레드가 ready_list에 삽입되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠든 스레드를 sleep_list로 삽입하는 thread_sleep()함수로 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 호출한 thread_sleep를 생성해야 하니 thread.h에서 함수 추가 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠든 스레드를 sleep_list에 삽입할 수 있는 함수를 선언이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스레드가 일어날 수 있는 시각정보인 ticks를 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sleep_list에 작은 ticks를 가진 스레드 순으로 정렬하여 삽입하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 스레드는 sleep이 필요하여 thread_block함수를 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로 그냥 정리하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Busy-waiting과 Sleep-Awake의 차이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Busy-waiting&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 조건이 만족될 때까지 끊임없이 상태를 체크하며 기다림.&lt;/li&gt;
&lt;li&gt;CPU 자원 낭비 및 다른 스레드의 실행 기회 감소.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Alarm Clock&lt;/b&gt; (Sleep-Awake 방식):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정된 시간이 지난 후에 작업을 수행.&lt;/li&gt;
&lt;li&gt;대기 상태에서 CPU 사이클을 사용하지 않아 자원 절약.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Busy-waiting 방식의 기존 코드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;timer_sleep()&lt;/b&gt; 함수: 스레드를 일정 시간동안 중지.&lt;/li&gt;
&lt;li&gt;현재 구현: 타이머 시간과 ticks 비교 후, **thread_yield()**를 통해 CPU 양보.&lt;/li&gt;
&lt;li&gt;문제점: 불필요한 context switching 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Sleep-Awake 방식으로의 변환&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;변경 목표&lt;/b&gt;: ticks에 도달하지 않은 스레드가 깨어나지 않도록 하여 효율성 증대.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 방안&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Sleep List의 도입&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;sleep_list&lt;/b&gt;: ticks에 도달하지 않은 스레드 저장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;thread_init&lt;/b&gt;에서 초기화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thread 구조체 수정&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;wakeup_ticks&lt;/b&gt; 필드 추가: 스레드가 깨어날 시각 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스레드 재우기 로직 변경&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;timer_sleep&lt;/b&gt;에서 &lt;b&gt;thread_sleep&lt;/b&gt; 호출: 스레드를 &lt;b&gt;sleep_list&lt;/b&gt;에 추가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;thread_sleep&lt;/b&gt;: &lt;b&gt;sleep_list&lt;/b&gt;에 스레드 추가 및 &lt;b&gt;thread_block&lt;/b&gt; 호출.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스레드 깨우기 로직 추가&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;timer_interrupt&lt;/b&gt;: 매 tick마다 &lt;b&gt;thread_wakeup&lt;/b&gt; 호출.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;thread_wakeup&lt;/b&gt;: &lt;b&gt;current_ticks&lt;/b&gt; 이상의 &lt;b&gt;wakeup_ticks&lt;/b&gt;를 가진 스레드를 &lt;b&gt;ready_list&lt;/b&gt;로 이동.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 순서로 Alarm Clock 기능을 개선할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Priority Scheduling은 우선순위가 높은 스레드가 CPU를 먼저 점유하도록 하는 스케줄링 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하기 위해 다음 절차를 밟아야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Ready List 정렬&lt;/b&gt;: 스레드들은 우선순위에 따라 ready list에 추가. list_insert_ordered 함수를 사용하여 우선순위가 높은 스레드가 리스트 앞부분에 위치하도록 정렬. cmp_thread_priority 함수는 스레드 우선순위를 비교하는 데 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Preempt (선점)&lt;/b&gt;: 현재 실행 중인 스레드의 우선순위보다 높은 스레드가 ready list에 추가되면 현재 스레드는 CPU를 양도. 이를 위해 preempt_priority 함수를 선언하고, ready list와 현재 실행중인 스레드의 우선순위를 비교하여 필요한 경우 thread_yield를 호출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lock Waiters 정렬&lt;/b&gt;: lock, semaphore, condition variable을 사용할 때 대기 중인 스레드들도 우선순위에 따라 정렬. 세마포어나 조건 변수의 waiters 목록을 cmp_thread_priority 또는 cmp_sema_priority 함수를 사용하여 우선순위 순으로 정렬. 이렇게 하면 우선순위가 가장 높은 스레드가 먼저 깨어남.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식대로 구현하면 우선순위 스케줄링이 대략적으로 구현된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Priority Donation은 높은 우선순위 스레드가 낮은 우선순위 스레드가 점유한 lock을 기다릴 때 발생하는 Priority Inversion 문제를 해결하기 위해 사용되는 기법이다. 구현 방법은 대략 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;스레드 구조체 수정&lt;/b&gt;: 스레드 구조체에 필요한 필드를 추가 이는 기본 우선순위(init_priority), 현재 대기 중인 lock(wait_on_lock), 기부 받은 우선순위 목록(donations), 기부 목록에 사용되는 리스트 요소(donation_elem).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lock 요청 시 Priority Donation&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lock을 요청할 때 이미 다른 스레드가 점유하고 있다면, 해당 스레드에게 우선순위를 기부.&lt;/li&gt;
&lt;li&gt;기부받는 스레드의 donations 리스트에 현재 스레드를 추가합니다. 이 때, donation_elem을 사용하며, 우선순위 순으로 정렬.&lt;/li&gt;
&lt;li&gt;donate_priority 함수를 통해 재귀적으로 우선순위 기부가 이루어 짐. 이는 다른 스레드가 또 다른 lock을 기다리고 있을 때 해당 lock의 소유자에게도 우선순위를 전달.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lock 반환 시 Priority 복귀&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lock을 반환할 때, 해당 lock을 기다리던 스레드의 기부를 철회.&lt;/li&gt;
&lt;li&gt;remove_donor 함수로 해당 lock을 기다리던 스레드를 donations 목록에서 제거.&lt;/li&gt;
&lt;li&gt;update_priority_for_donations 함수를 통해 현재 스레드의 우선순위를 다시 계산하여, 기부받은 우선순위가 없다면 초기 우선순위로, 있으면 가장 높은 기부받은 우선순위로 조정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선순위 변경 시 처리&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드의 우선순위가 변경되면, thread_set_priority 함수를 통해 기본 우선순위를 변경하고, update_priority_for_donations 함수로 기부 목록을 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사항들을 코드로 구현하는 부분들이 어렵고, 예외상황이 많이 발생하지만, 대략적으로 구현하면 위와 같다라는 것만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 개인적으로 위 사항들을 구현해보면서, 흐름들을 정리하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 흐름들을 정리하기 위해서 tests/threads 에 있는 코드들을 많이 참고하면서 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_D803A9FE2F92-1.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MWdie/btsA7xPAYfW/BlHbRDkCIgFEw79mj4QFkK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MWdie/btsA7xPAYfW/BlHbRDkCIgFEw79mj4QFkK/img.jpg&quot; data-alt=&quot;Thread가 실행되고 끝나는 시점까지의 시나리오를 그려봄&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MWdie/btsA7xPAYfW/BlHbRDkCIgFEw79mj4QFkK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMWdie%2FbtsA7xPAYfW%2FBlHbRDkCIgFEw79mj4QFkK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1142&quot; data-filename=&quot;IMG_D803A9FE2F92-1.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1142&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Thread가 실행되고 끝나는 시점까지의 시나리오를 그려봄&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_48C1CA3C85F2-1.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjqxGW/btsA5gN78JW/M0ky4yVmxPPHVPllc2HPTk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjqxGW/btsA5gN78JW/M0ky4yVmxPPHVPllc2HPTk/img.jpg&quot; data-alt=&quot;스레드가 공유자원을 접근할 때 세마포어 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjqxGW/btsA5gN78JW/M0ky4yVmxPPHVPllc2HPTk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjqxGW%2FbtsA5gN78JW%2FM0ky4yVmxPPHVPllc2HPTk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1094&quot; data-filename=&quot;IMG_48C1CA3C85F2-1.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드가 공유자원을 접근할 때 세마포어 시나리오&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_DF2ABB53CA5B-1.jpeg&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0gZ7x/btsA5hGceDp/5lfNkkToSuioNU6vSUej81/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0gZ7x/btsA5hGceDp/5lfNkkToSuioNU6vSUej81/img.jpg&quot; data-alt=&quot;스레드가 lock 요청 시 Priority에 의한 통제 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0gZ7x/btsA5hGceDp/5lfNkkToSuioNU6vSUej81/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0gZ7x%2FbtsA5hGceDp%2F5lfNkkToSuioNU6vSUej81%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1027&quot; height=&quot;1000&quot; data-filename=&quot;IMG_DF2ABB53CA5B-1.jpeg&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드가 lock 요청 시 Priority에 의한 통제 시나리오&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_166AB661C20F-1.jpeg&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbpnBv/btsA6R8DVuo/CpVMa1029wh2NwdDsXvV20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbpnBv/btsA6R8DVuo/CpVMa1029wh2NwdDsXvV20/img.jpg&quot; data-alt=&quot;condition variable 기법이 실행되는 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbpnBv/btsA6R8DVuo/CpVMa1029wh2NwdDsXvV20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbpnBv%2FbtsA6R8DVuo%2FCpVMa1029wh2NwdDsXvV20%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1416&quot; height=&quot;1000&quot; data-filename=&quot;IMG_166AB661C20F-1.jpeg&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;condition variable 기법이 실행되는 시나리오&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_DE399BEB52B9-1.jpeg&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Wsfd/btsA4QIVOC5/p5PWClKZbg7PwErYHsPbr1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Wsfd/btsA4QIVOC5/p5PWClKZbg7PwErYHsPbr1/img.jpg&quot; data-alt=&quot;스레드가 lock 요청 시 Priority에 의한 통제 시나리오 2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Wsfd/btsA4QIVOC5/p5PWClKZbg7PwErYHsPbr1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Wsfd%2FbtsA4QIVOC5%2Fp5PWClKZbg7PwErYHsPbr1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1151&quot; height=&quot;1000&quot; data-filename=&quot;IMG_DE399BEB52B9-1.jpeg&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드가 lock 요청 시 Priority에 의한 통제 시나리오 2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>일상/정글</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/27</guid>
      <comments>https://park-dev-diary.tistory.com/27#entry27comment</comments>
      <pubDate>Mon, 27 Nov 2023 20:51:24 +0900</pubDate>
    </item>
    <item>
      <title>Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(2)</title>
      <link>https://park-dev-diary.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://park-dev-diary.tistory.com/25&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1700812577065&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1)&quot; data-og-description=&quot;https://park-dev-diary.tistory.com/21 Eventsim 빌드 및 실행하기 이 모든 과정은 MacOS 기준을 작성되었습니다! 1. JAVA 설치하기 터미널에서 java -version 명령어로 설치되었는지 확인 후 없다면 아래 사이트를 &quot; data-og-host=&quot;park-dev-diary.tistory.com&quot; data-og-source-url=&quot;https://park-dev-diary.tistory.com/25&quot; data-og-url=&quot;https://park-dev-diary.tistory.com/25&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b3BMIe/hyUChFOtWu/bvRlvm8kEEYMhSQfGDGxhk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bnA6aa/hyUCePPsRZ/o72Hho995SkuesJKktYL2k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/u1d5b/hyUCgtl4t7/e8UxT9QZM2H7Lw5NjzKQuK/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822&quot;&gt;&lt;a href=&quot;https://park-dev-diary.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://park-dev-diary.tistory.com/25&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b3BMIe/hyUChFOtWu/bvRlvm8kEEYMhSQfGDGxhk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bnA6aa/hyUCePPsRZ/o72Hho995SkuesJKktYL2k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/u1d5b/hyUCgtl4t7/e8UxT9QZM2H7Lw5NjzKQuK/img.jpg?width=1018&amp;amp;height=822&amp;amp;face=0_0_1018_822');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Eventsim으로 생성한 로그데이터로 SuperSet으로 시각화하기(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://park-dev-diary.tistory.com/21 Eventsim 빌드 및 실행하기 이 모든 과정은 MacOS 기준을 작성되었습니다! 1. JAVA 설치하기 터미널에서 java -version 명령어로 설치되었는지 확인 후 없다면 아래 사이트를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;park-dev-diary.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 시각화 진행할 예정이였는데, 위에서 해준대로 데이터 마이그레이션하면 데이터 INSERT 도중에 에러가 나서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옮겨야할 데이터를 전부 가져오지 못하는 상황이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 당장 급하게 찾은 해결방안은 chunk_size 만큼 지정해서, 저장하는 도중 오류가 발생하더라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적으로 INSERT되는 코드를 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700812938327&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
from sqlalchemy import create_engine, types
import time

# 시작 시간 기록
start_time = time.time()

# JSON 파일 로드
file_path = 'test.data.json'  # JSON 파일 경로
df = pd.read_json(file_path, lines=True)

# 데이터 정제 및 전처리 (예시)
# df['new_column'] = df['existing_column'].apply(some_function)  # 새로운 컬럼 추가 또는 변환

# MySQL 데이터베이스 연결
database_username = 'root'
database_password = 'my-secret-pw'
database_ip       = 'localhost'
database_name     = 'mydatabase'
database_connection = create_engine(f'mysql+pymysql://{database_username}:{database_password}@{database_ip}/{database_name}')

table_name = 'user_song_count'


dtype={
    'ts': types.BigInteger,
    'userId': types.VARCHAR(length=255),
    'sessionId': types.Integer,
    'page': types.VARCHAR(length=255),
    'auth': types.VARCHAR(length=255),
    'method': types.VARCHAR(length=255),
    'status': types.Integer,
    'level': types.VARCHAR(length=255),
    'itemInSession': types.Integer,
    'location': types.VARCHAR(length=255),
    'userAgent': types.VARCHAR(length=255),
    'lastName': types.VARCHAR(length=255),
    'firstName': types.VARCHAR(length=255),
    'registration': types.BigInteger,
    'gender': types.VARCHAR(length=255),
    'tag': types.VARCHAR(length=255),
    'artist': types.VARCHAR(length=500),
    'song': types.VARCHAR(length=255),
    'length': types.Float
}

# 데이터베이스에 데이터 저장
chunk_size = 100  # 적절한 청크 크기 설정
for i in range(0, len(df), chunk_size):
    chunk = df[i:i + chunk_size]
    try:
        chunk.to_sql(con=database_connection, name=table_name, if_exists='append', index=False, dtype=dtype, chunksize=chunk_size)
    except Exception as e:
        print(f&quot;Error occurred with chunk starting at row {i}: {e}&quot;)
        # 오류 발생 시 로그를 남기고 다음 청크로 계속 진행
        continue


# 실행 완료 후 경과 시간 출력
end_time = time.time()
print(f&quot;Time taken: {end_time - start_time} seconds&quot;)

# 데이터베이스 연결 종료 (선택적)
# database_connection.dispose()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 수정하고 데이터의 대부분을 INSERT 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 찝찝해서 해당 부분에서 발생하고 있는 오류들을 바라보니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 대상인 데이터 파일에 artist의 길이가 255보다 더 길어지는 데이터 값이 있었던 모양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선 artist length값도 조금 더 증가시켜줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 일단 데이터를 추가하는 것은 완료하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터의 크기가 조금 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트심으로 만든 데이터크기가 대략적으로 500MB정도 되는데, 이걸 10배 정도 더 키워서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 결정적으로 시각적인 데이터로 표현할 때, 어느정도로 부화가 있는지를 체크하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이벤트심을 통해 다시 한번 데이터를 생성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 작업을 반복하여 데이터를 MySQL로 마이그레이션 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1700833827139&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bin/eventsim --config examples/example-config.json --tag contral -n 25000 --start-time &quot;2015-06-01T00:00:00&quot; --end-time &quot;2015-12-01T00:00:00&quot; --growth-rate 0.25 --userid 1 --randomseed 1 event.data.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;End-time과 contral -n (유저수) 인자값만 수정하면 기존 default 데이터보다 훨씬 많은 데이터들을 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 만들어진 데이터의 용량은 대략 7기가 정도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 데이터를 다시 한번 Pandas를 활용해서 MySQL에 넣어주면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하고 진행한 결과 우선 Pandas으로 7기가 이상을 MySQL에 INSERT 작업을 하는 중에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적으로 Killed 되어서 DB로 넣어지지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 Python에서 커다란 데이터를 pandas로 데이터프레임을 만드는 과정에서 너무 커버리면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 아웃이 발생하여 killed 된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 청크로도 쪼개보고, df에서 파라미터에 chunk 값도 넣어봐주고, json를 스트림으로 저장할 수 있게 도와주는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ijson를 설치해서 저장도 해보고, 버퍼개념을 활용해서 실행해봤지만, 소용없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 나의 삽질이 녹아있는 코드이다. 전부 killed이 발생하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1700895863577&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ijson
import pandas as pd
from sqlalchemy import create_engine, types
from sqlalchemy.exc import SQLAlchemyError
import time

# 시작 시간 기록
start_time = time.time()

# JSON 파일 로드
file_path = 'event.data.json'  # JSON 파일 경로
df = pd.read_json(file_path, lines=True)

# 데이터 정제 및 전처리 (예시)
# df['new_column'] = df['existing_column'].apply(some_function)  # 새로운 컬럼 추가 또는 변환

# MySQL 데이터베이스 연결
database_username = 'root'
database_password = 'my-secret-pw'
database_ip       = 'localhost'
database_name     = 'mydatabase'
database_connection = create_engine(f'mysql+pymysql://{database_username}:{database_password}@{database_ip}/{database_name}')

table_name = 'user_song_count'


dtype={
    'ts': types.BigInteger,
    'userId': types.VARCHAR(length=255),
    'sessionId': types.Integer,
    'page': types.VARCHAR(length=255),
    'auth': types.VARCHAR(length=255),
    'method': types.VARCHAR(length=255),
    'status': types.Integer,
    'level': types.VARCHAR(length=255),
    'itemInSession': types.Integer,
    'location': types.VARCHAR(length=255),
    'userAgent': types.VARCHAR(length=255),
    'lastName': types.VARCHAR(length=255),
    'firstName': types.VARCHAR(length=255),
    'registration': types.BigInteger,
    'gender': types.VARCHAR(length=255),
    'tag': types.VARCHAR(length=255),
    'artist': types.VARCHAR(length=255),
    'song': types.VARCHAR(length=255),
    'length': types.Float
}


buffer = []  # 아이템을 저장할 버퍼
buffer_size = 1000  # 버퍼 크기 설정


with open(file_path, 'r') as file:
    for item in ijson.items(file, 'item'):
        buffer.append(item)
        if len(buffer) &amp;gt;= buffer_size:
            # DataFrame 생성
            df = pd.DataFrame(buffer)
            # 일괄 처리를 위한 SQL 문 생성
            insert_statement = pd.io.sql.get_insert_statement(table_name='user_song_count', con=database_connection, schema=None, if_exists='append', index=False, dtype=dtype)
            # 데이터베이스에 일괄 삽입
            try:
                database_connection.execute(insert_statement, df.to_dict(orient='records'))
            except SQLAlchemyError as e:
                print(f&quot;Error occurred: {e}&quot;)
            buffer = []  # 버퍼 초기화

    # 남은 데이터 처리
    if buffer:
        df = pd.DataFrame(buffer)
        insert_statement = pd.io.sql.get_insert_statement(table_name='user_song_count', con=database_connection, schema=None, if_exists='append', index=False, dtype=dtype)
        try:
            database_connection.execute(insert_statement, df.to_dict(orient='records'))
        except SQLAlchemyError as e:
            print(f&quot;Error occurred: {e}&quot;)

# with open('event.data.json', 'r') as file:
#     for item in ijson.items(file, 'item'):
#         buffer.append(item)
#         if len(buffer) &amp;gt;= buffer_size:
#             df = pd.DataFrame(buffer)
#             df.to_sql(con=database_connection, name='user_song_count', if_exists='append', index=False)
#             buffer = []  # 버퍼 초기화

#     if buffer:  # 남은 아이템 처리
#         df = pd.DataFrame(buffer)
#         df.to_sql(con=database_connection, name='user_song_count', if_exists='append', index=False)

# with open('event.data.json', 'r') as file:
#     # ijson은 파일을 스트리밍하며 'item' 이벤트를 발생시킵니다.
#     for item in ijson.items(file, 'item'):
#         # 각 item을 DataFrame으로 변환
#         df = pd.DataFrame([item])
#         # 데이터베이스에 데이터 저장
#         df.to_sql(con=database_connection, name='user_song_count', if_exists='append', index=False)

# # 청크 크기 설정
# chunk_size = 1000  # 적절한 청크 크기 설정

# # JSON 파일을 청크 단위로 읽기
# for chunk in pd.read_json(file_path, lines=True, chunksize=chunk_size):
#     # 데이터베이스에 데이터 저장
#     try:
#         chunk.to_sql(con=database_connection, name=table_name, if_exists='append', index=False, dtype=dtype)
#     except Exception as e:
#         print(f&quot;Error occurred: {e}&quot;)
#         # 오류 발생 시 로그를 남기고 다음 청크로 계속 진행
#         continue


# # 데이터베이스에 데이터 저장
# chunk_size = 100  # 적절한 청크 크기 설정
# for i in range(0, len(df), chunk_size):
#     chunk = df[i:i + chunk_size]
#     try:
#         chunk.to_sql(con=database_connection, name=table_name, if_exists='append', index=False, dtype=dtype, chunksize=chunk_size)
#     except Exception as e:
#         print(f&quot;Error occurred with chunk starting at row {i}: {e}&quot;)
#         # 오류 발생 시 로그를 남기고 다음 청크로 계속 진행
#         continue


# 실행 완료 후 경과 시간 출력
end_time = time.time()
print(f&quot;Time taken: {end_time - start_time} seconds&quot;)

# 데이터베이스 연결 종료 (선택적)
# database_connection.dispose()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에 어떤 방법으로 해결할 수 있을까? 라는 생각을 하였는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애시당초 pandas로 굳이 MySQL에 적재를 해야하나 라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 json을 여러개로 쪼개서, 하나씩 넣어주는 방법도 있었을 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국엔 넣었던 방법은 pySpark로 실행하는 것이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Spark를 활용하여, 어떤 분산처리를 하여 데이터를 적재하거나, 빠르게 처리하기 위해서 사용한 것은 아니였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오로지 Spark가 데이터를 분산해서 Worker에게 쪼개주는 부분만 활용해서 적재할 수 있는 방법이 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 위해 Spark를 이용하게 된 것이다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 Python에 Pandas를 써버리면, Killed 당해버리니&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark를 활용하여 데이터 마이그레이션을 진행하였고, 결과적으로는 성공하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 조금 이해가 되지 않는 부분들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas를 활용해서 데이터를 적재하게 되었을 때, 파이썬의 스펙이 좋지 못해서 killed 당하는 건지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 Pandas에서 데이터프레임으로 Wrap하는 과정에서 메모리를 낭비하게 되는건지, 하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 메모리 낭비 부분을 충분히 청크파일 혹은 ijson으로 커버하였다고 생각했지만 이 방법도 통하지가 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark가 killed 되지 않은 이유는 대량 JSON를 RDD로 분산해서 데이터를 쪼개서 워커에게 나눠줄 수 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리를 효율적으로 관리할 수 있었고, JVM환경에서 실행되기 때문에 Pandas의 무분별한 메모리 낭비를 안할 수 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행이 되었던 것 같긴한데.. 솔직히 명확하게 잘 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 데이터 적재하고 카운트를 보니 천만정도 되는 수를 INSERT를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 7기가 정도 데이터를 MySQL에 적재해보니 세삼 이 정도의 크기를 가지고, 시각화 대시보드를 활용하기가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어려울 것 같고, 집계테이블을 따로 만들어, aggreate 해야하는데 어떤 데이터와 어떤 방식으로 진행할지 고민이 필요할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 aggreate 작업이 되어야 시각화 대쉬보드가 탄생할 수 있을 것 같다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오픈소스</category>
      <author>JMDev</author>
      <guid isPermaLink="true">https://park-dev-diary.tistory.com/26</guid>
      <comments>https://park-dev-diary.tistory.com/26#entry26comment</comments>
      <pubDate>Sat, 25 Nov 2023 20:53:50 +0900</pubDate>
    </item>
  </channel>
</rss>