P
PipeCraft

Roadmap Progress

ข้อมูลเบื้องต้นเกี่ยวกับวิศวกรรมข้อมูล (Introduction to Data Engineering) Python สำหรับ Data Engineering พื้นฐาน Linux & CLI สำหรับวิศวกรข้อมูล Git สำหรับระบบงานข้อมูลและทีมพัฒนา ระบบเครือข่ายและระบบประมวลผลแบบกระจายศูนย์ (Networking & Distributed Systems) SQL ขั้นสูง (Window Functions & Optimization) ฐานข้อมูลไม่ใช่เชิงสัมพันธ์ (NoSQL Databases & Modern Stores) การออกแบบโครงสร้างข้อมูล (Star/Snowflake & SCD) โครงสร้างพื้นฐานเครือข่ายระบบคลาวด์ (Cloud-based Networking) สถาปัตยกรรม Data Lakehouse (Apache Iceberg & MinIO) Local-first Data Engineering ด้วย DuckDB โครงสร้างพื้นฐานในรูปแบบโค้ด (IaC ด้วย Terraform) การแปลงข้อมูลระดับโปรด้วย dbt-core ข้อตกลงร่วมด้านข้อมูล (Data Contracts) การบริการและการส่งต่อข้อมูลวิเคราะห์ (Data Serving & Reverse ETL) การประมวลผลข้อมูลขนาดใหญ่แบบกระจายด้วย Apache Spark ระบบจัดการเวิร์กโฟลว์ข้อมูล (Airflow, Dagster & Prefect) การประมวลผลข้อมูลแบบเรียลไทม์ด้วย Kafka & Redpanda การจัดการคอนเทนเนอร์และคลัสเตอร์ (Containers & Kubernetes) ระบบ CI/CD และการเฝ้าระวังคุณภาพข้อมูล (CI/CD, Monitoring & Testing) ระบบความปลอดภัยและการควบคุมข้อมูล (Security, Governance & Privacy) ระบบปฏิบัติการและประมวลผลโมเดล (Machine Learning & MLOps)
บทที่ 1: รากฐานวิศวกรรมซอฟต์แวร์และระบบ (Foundations of Software & Systems)

Python สำหรับ Data Engineering

1.2 การจัดการหน่วยความจำและการทำงานพร้อมกันในภาษา Python

การพัฒนาท่อส่งข้อมูลด้วยภาษา Python ที่มีความเร็วในระดับระบบอุตสาหกรรม จำเป็นต้องเข้าใจลึกซึ้งถึงกลไกภายในของล่ามภาษา CPython โดยเฉพาะการจัดสรรพื้นที่หน่วยความจำ และการออกแบบระบบทำงานพร้อมกัน (Concurrency) เพื่อก้าวข้ามข้อจำกัดทางสถาปัตยกรรม

1. โครงสร้างหน่วยความจำและการจัดสรรผ่าน PyMalloc

ทฤษฎีและกลไกการทำงาน (How it works): ในภาษา Python (CPython) วัตถุขนาดเล็กที่มีขนาดไม่เกิน 512 ไบต์ จะไม่ถูกร้องขอผ่านฟังก์ชัน malloc มาตรฐานของระบบปฏิบัติการโดยตรง เพื่อป้องกันปัญหาการหน่วงของสัญญาณระบบ (System Call Overhead) และการเกิดเศษซากรูในแรม (Memory Fragmentation) CPython จึงพัฒนาตัวจัดสรรหน่วยความจำชื่อ PyMalloc โครงสร้างการจัดเก็บของ PyMallocแบ่งออกเป็น 3 ชั้น: 1. **Block**: ขนาดคงที่ทวีคูณของ 8 ไบต์ (เช่น 8, 16, 24, จนถึง 512 ไบต์) สำหรับวางค่าวัตถุจริง 2. **Pool**: กลุ่มของบล็อกขนาดเดียวกัน มีขนาดรวมคงที่ 4KB ซึ่งตรงกับหน้าหน่วยความจำจริง (Virtual Memory Page Size) ของ OS 3. **Arena**: บล็อกหน่วยความจำหลักขนาด 256KB ที่จัดสรรตรงจากเคอร์เนลระบบปฏิบัติการ ประกอบด้วย Pool ย่อย ๆ ทั้งหมด 64 พูล

# Inspecting Python object memory allocation size in bytes
import sys

small_int = 42
large_list = [i for i in range(100000)]

print(f"Size of small integer: {sys.getsizeof(small_int)} bytes")
print(f"Size of large list object envelope: {sys.getsizeof(large_list)} bytes")
                    

Use Case ในชีวิตจริง (Real-world Scenario): เมื่อเราเขียนโปรแกรมดึงข้อมูลและทำความสะอาดไฟล์ JSON ขนาดใหญ่ที่มีวัตถุขนาดเล็กนับล้านตัว หากตัวแปรเหล่านั้นถูกสร้างและลบตลอดเวลา PyMalloc จะทำการ Recycle บล็อกหน่วยความจำภายใน Pool ทันทีโดยไม่ต้องรบกวน OS ทำให้โปรแกรมประมวลผลเร็วขึ้นและแรมไม่พุ่งสูง

ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ถึงแม้ PyMalloc จะช่วยกู้แรมระดับบล็อก แต่แรมชั้นบนสุดอย่าง **Arena** จะไม่ถูกคืนให้ระบบปฏิบัติการหลักจนกว่าพูลย่อยทั้งหมดภายในตัวมันจะว่างเปล่า 100% ส่งผลให้แรมของโปรเซสหลักดูค้างคาแม้ลบตัวแปรไปหมดแล้ว (Memory Bloat) แก้ไขโดยสลายแรมสะสมด้วยการรันบนโพรเซสย่อยแล้วปิดตัวลง หรือจัดพูลการใช้งานตัวแปรผ่าน `multiprocessing`

2. วงจรชีวิตและการขจัดหน่วยความจำ (Reference Counting & Generational Garbage Collection)

ทฤษฎีและกลไกการทำงาน (How it works): CPython มีกลไกเคลียร์ขยะสองขั้น: - **Reference Counting (การนับการอ้างอิง)**: ทุกอ็อบเจกต์มีตัวแปรร่วมภายใน `ob_refcnt` เมื่อเราชี้ตัวแปรใหม่ไปหาอ็อบเจกต์ ค่าจะเพิ่มขึ้น 1 และลดลง 1 เมื่อตัวแปรหลุดขอบเขตการทำงาน (Scope) เมื่อค่าเหลือ 0 แรมจะถูกทิ้งและปลดปล่อยทันที - **Generational Garbage Collection**: ใช้จัดการปัญหา **Cyclic References** (วัตถุอ้างอิงเป็นวงกลม เช่น A ชี้ B และ B ชี้ A ทำให้ค่า RC ค้างที่ 1 ไม่มีทางเป็น 0) ตัวเก็บขยะจะแบ่งอ็อบเจกต์เป็น 3 เจนเนอเรชัน (Gen 0, 1, 2) ข้อมูลใหม่จะอยู่ Gen 0 และถูกตรวจสอบบ่อยสุด หากรอดจากการล้างขยะจะถูกย้ายขึ้นชั้นถัดไปตามลำดับ

# Managing Garbage Collector manually for critical memory paths
import gc

# Disable automatic collection to prevent performance hiccups during tight loops
gc.disable()

# Perform memory intensive tasks...
# Manually trigger Gen 2 cleanup after processing is done
gc.collect(2)
gc.enable()
                    

Use Case ในชีวิตจริง (Real-world Scenario): ในงานเขียนคลาสจัดการการแปลงโมเดลข้อมูลขนาดใหญ่ที่ต้องเก็บสถานะความสัมพันธ์ต่อกัน (เช่น Parent-Child Nodes) ซึ่งมักเกิด Cyclic Reference การปิดระบบสแกนขยะอัตโนมัติในระหว่างลูปอ่านข้อมูลล้านบรรทัด และใช้วิธีสั่งรัน `gc.collect()` ด้วยมือตอนท้ายสุดจะทำให้สปีดการทำงานเรียบเนียนขึ้นถึง 20%

ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การล้างขยะแบบอัตโนมัติอาจทำให้ระบบหน่วงกะทันหัน (Stop-the-world pauses) ในวินาทีที่ตัวสแกน Gen 2 เริ่มรัน แก้ไขโดยการแยกโครงสร้างตัวแปรไม่ให้ชี้วนซ้ำกัน หรือการประยุกต์ใช้ **Weak Reference** ผ่านไลบรารี `weakref` เพื่อให้อ้างอิงอ็อบเจกต์ได้โดยไม่เพิ่มคะแนน Reference Count

3. การทำงานขนานจริง (Multiprocessing) และข้อจำกัดของ GIL

ทฤษฎีและกลไกการทำงาน (How it works): **GIL (Global Interpreter Lock)** คือตัวล็อกระดับล่ามภาษาใน CPython เพื่อป้องกันไม่ให้มี Thread มากกว่าหนึ่งตัวทำงานประมวลผลไบต์โค้ดพร้อมกัน เพื่อความปลอดภัยในการนับยอด Reference Counting สถาปัตยกรรมในการรันขนานจึงแบ่งออกเป็น: - **Multi-threading**: รันภายใต้โพรเซสเดียว เหมาะกับงาน **I/O-Bound** (เช่น โหลดหน้าเว็บ คิวรีข้อมูลระบบคลาวด์) เพราะระหว่างรอระบบส่งค่ากลับ เธรดจะคายสิทธิ์ล็อก GIL ให้เธรดอื่นรันแทน - **Multi-processing**: เลี่ยง GIL โดยการบิวด์ระบบโพรเซสของระบบปฏิบัติการขึ้นมาใหม่ มีพื้นที่แรมแยกและ CPython VM ของตนเอง รันบนซีพียูคนละคอร์ขนานกันได้อย่างสมบูรณ์แบบ (ขนานแท้จริง)

# Executing CPU-bound tasks in parallel using ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import math

def calculate_heavy_prime(n):
    # Simulating intensive computation
    count = 0
    for i in range(2, n):
        if all(i % d != 0 for d in range(2, int(math.sqrt(i)) + 1)):
            count += 1
    return count

if __name__ == "__main__":
    tasks = [100000, 100001, 100002, 100003]
    with ProcessPoolExecutor(max_workers=4) as executor:
        results = list(executor.map(calculate_heavy_prime, tasks))
    print(f"Results computed in parallel: {results}")
                    

Use Case ในชีวิตจริง (Real-world Scenario): การนำเข้าไฟล์พิกัดแผนที่พนักงานขับรถขนาด 10GB ที่ต้องถอดรหัสและคำนวณระยะพิกัดเชิงคณิตศาสตร์ (CPU-bound) ทีมงานใช้ `multiprocessing` แยกดึงข้อมูลกระจายรันขนานบนเซิร์ฟเวอร์ 16 คอร์ ทำให้ระยะเวลาการประมวลผลลดลงจาก 4 ชั่วโมง เหลือเพียง 20 นาที

ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การแชร์ข้อมูลระหว่างโพรเซสต้องผ่านการแปลงไบนารีวัตถุ (**Serialization / Pickling**) ซึ่งมีอัตราการดึงหน่วยความจำ I/O และซีพียูโชหุ้ยสูง (IPC Overhead) หากเราส่งตารางขนาดใหญ่ข้ามโพรเซส โปรแกรมอาจทำงานช้ากว่าโพรเซสเดียว แก้ไขโดยให้แต่ละโพรเซสย่อยอ่านเขียนไฟล์ปลายทางของตนเองโดยตรง หรือประยุกต์ใช้หน่วยความจำร่วมกัน (Shared Memory)

4. การพัฒนาแบบไม่ซิงโครนัส (Asyncio) ในสตรีมมิ่งจ๊อบ

ทฤษฎีและกลไกการทำงาน (How it works): **Asyncio** นำเสนอการทำงานแบบร่วมมือพร้อมกันภายใต้เธรดเดียว (Cooperative Multitasking) โดยมีตัวควบคุมลูปเหตุการณ์ **Event Loop** คอยสลับงานฟังก์ชันที่รันค้างรอ I/O (เช่น รอข้อมูลอินเทอร์เน็ต) เมื่อฟังก์ชันพบคำสั่ง `await` ระบบจะคายสิทธิ์ให้ฟังก์ชันอื่นในคิวรันต่อทันทีโดยไม่ต้องรอระบบปฏิบัติการสลับบริบทเธรด (Context Switching) ซึ่งมีน้ำหนักเบาและกินแรมน้อยกว่าการใช้ Threading แบบปกติ

# Asynchronous parallel API ingestion client example using aiohttp
import asyncio
import aiohttp

async def fetch_api_page(session, url):
    async with session.get(url) as response:
        return await response.json()

async def main():
    urls = [f"https://api.pipecraft.dev/data?page={i}" for i in range(10)]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_api_page(session, url) for url in urls]
        # Execute all request pages concurrently
        results = await asyncio.gather(*tasks)
        print(f"Fetched {len(results)} API responses concurrently.")

if __name__ == "__main__":
    asyncio.run(main())
                    

Use Case ในชีวิตจริง (Real-world Scenario): ระบบดึงราคาสินทรัพย์การเงินเรียลไทม์จากปลายทางต่างประเทศ 50 แหล่งข้อมูลพร้อมกันเพื่อตรวจสอบความต่างของราคา (Arbitrage) การใช้ `asyncio` ร่วมกับ `aiohttp` ทำงานได้รวดเร็วเต็มประสิทธิภาพเน็ตเวิร์กโดยไม่เปลืองทรัพยากรเซิร์ฟเวอร์

ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): หากมีโค้ดส่วนที่เป็นการคำนวณเชิงลึกแบบบล็อกการทำงาน (Blocking CPU-Bound Task) ไปรันอยู่ในลูปหลักของ Asyncio จะส่งผลให้ระบบงานทั้งหมดหยุดชะงัก (Event Loop Block) แก้ไขโดยโยนงานบล็อกนั้นไปรันข้างนอกผ่าน Thread/Process pool ด้วยคำสั่ง `loop.run_in_executor()`

5. การจัดการสิ่งเกี่ยวเนื่อง (Dependency Management) และการบรรจุโปรเจกต์ด้วย Poetry

ทฤษฎีและกลไกการทำงาน (How it works): การเขียนท่อส่งข้อมูลข้อมูลขนาดใหญ่จำเป็นต้องใช้ไลบรารีภายนอกร่วมกันหลายสิบตัว (เช่น Pandas, PySpark, Great Expectations) การใช้ `pip install` แบบดั้งเดิมมักเกิดปัญหาเวอร์ชันขัดแย้งกัน (Dependency Hell) **Poetry** นำเสนอกระบวนการควบคุมแบบกำหนดเจาะจงผ่านไฟล์ `pyproject.toml` และไฟล์ล็อกเวอร์ชันแบบคงที่ `poetry.lock` เพื่อการันตีว่าทุกสภาพแวดล้อมตั้งแต่การพัฒนา (Local Dev), การทดสอบ (CI/CD) ไปจนถึงงานจริง (Production) จะรันบนแพ็กเกจชุดเดียวกันอย่างถูกต้องแม่นยำ

# Complete production-ready pyproject.toml configuration template
[tool.poetry]
name = "pipecraft-elt-pipeline"
version = "1.0.0"
description = "Production data ingestion and validation pipeline"
authors = ["Data Platform Team "]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
pandas = "2.1.1"
polars = "0.19.3"
pydantic = "^2.4"
great-expectations = "0.17.15"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
black = "^23.7.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
                    

Use Case ในชีวิตจริง (Real-world Scenario): ทีมผู้ดูแลแพลตฟอร์มคลังข้อมูลของห้างสรรพสินค้าใช้ Poetry คุมสเปคเวอร์ชันของอิมเมจท่อส่งข้อมูลทั้งหมด ทำให้เมื่อรันบนเครื่อง Airflow Worker ข้ามเครื่องจะไม่มีการดาวน์โหลดไลบรารีหลุดเวอร์ชันที่ส่งผลให้การรันจ๊อบล้มเหลว

ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ไฟล์ล็อก `poetry.lock` หากไม่มีการคอมมิตเข้าสู่ระบบควบคุมเวอร์ชัน (Git) จะทำให้ทีมอื่นดาวน์โหลดเวอร์ชันใหม่ที่อาจเกิดบั๊กพังได้ แก้ไขโดยต้องทำการคอมมิตไฟล์ล็อกและ pyproject.toml เข้า Git เสมอ และหลีกเลี่ยงการอัปเดตเวอร์ชันบน Production โดยตรง ให้รัน `poetry update` บน Local และทดสอบผ่าน CI/CD ก่อนใช้งานจริง

🛠️ Weekend Sandbox Challenge: Multiprocessing Log Parser

โจทย์ปฏิบัติการ: จงเขียนโปรแกรม Python ประมวลผลขนาน (Parallel Processing) เพื่อทำระบบอ่านวิเคราะห์หาข้อมูล Error จากไฟล์ล็อกจำลอง โดยใช้ `ProcessPoolExecutor` เพื่อสลับข้ามข้อจำกัดของ GIL ในการรันงานระดับ CPU-bound

# parallel_log_parser.py
from concurrent.futures import ProcessPoolExecutor
import re

log_lines_chunk_1 = ["[INFO] User logged in", "[ERROR] Connection timeout", "[INFO] Query executed"]
log_lines_chunk_2 = ["[INFO] Disk check ok", "[ERROR] Database deadlock", "[WARNING] Slow response"]

def parse_errors(lines):
    errors = []
    for line in lines:
        if "[ERROR]" in line:
            errors.append(line)
    return errors

if __name__ == "__main__":
    chunks = [log_lines_chunk_1, log_lines_chunk_2]
    with ProcessPoolExecutor(max_workers=2) as executor:
        results = executor.map(parse_errors, chunks)
    
    all_errors = [err for sublist in results for err in sublist]
    print(f"Total Errors Found: {len(all_errors)} -> {all_errors}")
                    

💼 Senior Technical Interview Q&A

Q1: กลไก CPython GIL (Global Interpreter Lock) คืออะไร และมีผลกระทบอย่างไรต่อประสิทธิภาพท่อส่งข้อมูลที่เขียนด้วย Python?

A1: GIL คือตัวล็อกประเภท Mutex ของ CPython ที่จำกัดให้เทรดใช้งาน (Thread) รันประมวลผลโค้ดทีละหนึ่งคำสั่งในเวลานั้นเพื่อความปลอดภัยของตัวแปรหน่วยความจำ ส่งผลให้การทำ Multi-threading ไม่สามารถเร่งความเร็วงานประเภทคำนวณหนัก (CPU-bound) ได้ ทางแก้คือย้ายไปใช้ Multi-processing ซึ่งเป็นการรันแยกโปรเซสที่มีอินสแตนซ์ CPython และแรมส่วนตัวของตัวเอง

Q2: เพราะเหตุใดตัวรวบรวมข้อมูลจำพวก Generators (`yield`) จึงเหมาะสมกว่าการสร้าง Lists ในการประมวลผลไฟล์ขนาด 100GB?

A2: การสร้าง List ปกติจะดาวน์โหลดและจองพื้นที่ข้อมูลทั้งหมดใส่ลงในแรม (RAM) พร้อมกัน ซึ่งจะทำให้เครื่องวิเคราะห์เกิดปัญหาแรมเต็มพังเสียหาย (OOM) ในขณะที่ Generators จะค่อยๆ ทยอยป้อนข้อมูลออกมาทีละแถวประมวลผลแบบขยับทีละสเต็ป (Lazy Evaluation) ใช้พื้นที่หน่วยความจำคงที่ต่ำมากในการประมวลผลไฟล์ไม่ว่าจะมีขนาดใหญ่เพียงใด

🐍 Python Interactive Data Cleaner

Interactive Simulator

เป้าหมาย: เขียนฟังก์ชันแปลงข้อมูลเพื่อคลีนคอลัมน์ email ให้เป็นพิมพ์เล็กทั้งหมด และเติมค่าว่างในคอลัมน์ age ด้วยค่าเฉลี่ย (Mean = 28)

📋 ข้อมูลดิบก่อนประมวลผล (Raw Input DataFrame):
ID Name Email Age
1 John Doe JOHN@Example.com 32
2 Jane Smith Jane.S@Gmail.com NaN
3 Bob Johnson BOB_J@Yahoo.Com 24
📝 เขียนคำสั่ง Pandas (Editable Python Code):
# เติมคำสั่งในช่องว่างเพื่อทำความสะอาดข้อมูล
import pandas as pd
df = pd.DataFrame(raw_data)
df['email'] = df['email'].str. ()
df['age'] = df['age']. (28)
print(df.to_string())

ข้อดี / จุดเด่น & ข้อเสีย / ข้อควรระวัง

ข้อดี / จุดเด่น

  • ไลบรารีวิเคราะห์ข้อมูลหนาแน่น พัฒนาโค้ดได้รวดเร็วมาก
  • เชื่อมต่อง่ายกับระบบ Database และ Cloud API แทบทุกประเภท

ข้อเสีย / ข้อควรระวัง

  • ประมวลผลข้อมูลระดับหลายร้อยกิกะไบต์ได้ช้าเนื่องจากข้อจำกัดของหน่วยความจำเครื่องเดี่ยว
  • มีปัญหาคอขวด CPU จากการทำงานแบบ Single-thread (Global Interpreter Lock - GIL)

ปฏิบัติการจริง (Lab Practice)

Bilingual Guide

แล็บ: สร้าง ETL pipeline แบบง่ายด้วย Python

💡 คำแนะนำ & ทริกเด็ด

การประมวลผลข้อมูลใน Pandas ให้หลีกเลี่ยงการวนลูปแบบ Iterrows() เพราะทำงานช้ามาก ให้เลือกใช้ Vectorized Operations หรือ .apply() แทน ซึ่งเร็วกว่าหลักร้อยเท่า