Skip to main content

Data Engineering Practices

ออกแบบให้งานสามารถทำซ้ำ (Reproducible) ได้

ความท้าทายอย่างหนึ่งในการสร้าง Data Pipelines คือการออกแบบให้งานแต่ละงานนั้นสามารถทำซ้ำ หรือ Reproducible ได้ ซึ่งนั่นก็หมายความว่าเราสามารถที่จะรัน Pipeline ของเราใหม่ไม่ว่าจะกี่ครั้ง หรือไม่ว่าจะเวลาใดก็ตาม เราสามารถที่จะคาดหวังผลลัพธ์สุดท้ายได้เสมอ

เขียนฟังก์ชั่นการทำงานให้มีสมบัติ Idempotent

Idempotent เป็นสมบัติอย่างหนึ่งของฟังก์ชั่นทางคณิตศาสตร์ เราจะได้ผลลัพธ์เป็นค่าเดิมเสมอ ไม่ว่าเราจะดำเนินการกี่ครั้งแล้วก็ตาม ตรงนี้เป็นส่วนสำคัญในการสร้าง Data Pipelines เลยทีเดียว

สมมุติว่าเรากำลังเขียนฟังก์ชั่นหนึ่งใน Python อยู่

>>> fruits = ["Apple", "Orange", "Grape"]
>>>
>>>
>>> def add(fruit):
... fruits.append(fruit)
...
>>>

หลังจากนั้น เราเอาฟังก์ชั่น add มาใช้งานต่อ ก็จะได้ผลลัพธ์ตามนี้

>>> add("Pineapple")
>>> print(fruits)
['Apple', 'Orange', 'Grape', 'Pineapple']

ซึ่งดูแล้วก็ไม่แปลกอะไรเนอะ แต่จะแปลกทันทีเมื่อเราใช้ฟังก์ชั่น add ซ้ำเข้าไปอีก

>>> add("Pineapple")
>>> add("Pineapple")
>>> add("Pineapple")
>>> add("Pineapple")
>>> print(fruits)
['Apple', 'Orange', 'Grape', 'Pineapple', 'Pineapple', 'Pineapple', 'Pineapple', 'Pineapple']

จะเห็นว่าได้มี Pineapple เพิ่มขึ้นมาอีก 4 ค่า แบบนี้เราบอกได้เลยว่าฟังก์ชั่นนี้ "ไม่ idemponent" ครับ และเราไม่ควรสร้าง data pipeline ที่มีฟังก์ชั่นแบบนี้

Data pipeline ที่ดี เราควรที่จะสามารถรันมันซ้ำกี่รอบก็ได้ ผลที่ได้ควรจะได้เหมือนเดิม จะส่งผลให้เราสามารถที่จะทดสอบการทำงานของ data pipeline เราได้ง่าย ดูแลบำรุงรักษาได้ง่ายอีกด้วย ลองนึกสภาพว่าถ้าเรารัน data pipeline เรื่อยๆ มี input ที่เหมือนเดิม แต่ได้ผลไม่เหมือนกันสักรอบ แบบนี้ต้องเรียกว่าวัดดวงกันล่ะ ต้องมีสักรอบที่ทำงานถูกต้อง อะไรประมาณนี้ ก็คงไม่ดีแน่ๆ

การทำให้ data pipeline ของเรามีคุณสมบัติ idemponent ก็ยังมีข้อดีอีกคือ ข้อมูลเราจะไม่ซ้ำซ้อน มีข้อมูลที่สดใหม่ (ไม่มีพวกข้อมูลที่เป็น stale หรือข้อมูลที่ค้างอยู่ในระบบที่เราไม่ได้ใช้งานแล้ว) แล้วก็จริงๆ ยังช่วยประหยัดเนื้อที่ในการเก็บข้อมูลอีกด้วย เพราะเราไม่ได้เก็บข้อมูลซ้ำเข้ามา

ทีนี้จากโค้ดด้านบนเราปรับให้มีคุณสมบัติ idemponent ได้อย่างไร?

ทำแบบนี้เลย แทนที่เราจะใช้ append เราเปลี่ยนมาใช้ + แทน

>>> fruits = ['Apple', 'Orange', 'Grape']
>>> def add(fruit):
... return fruits + [fruit]
...
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> new_fruits = add("Pineapple")
>>> print(new_fruits)
['Apple', 'Orange', 'Grape', 'Pineapple']

เห็นได้ชัดเลยว่า ไม่ว่าเราจะเรียก add กี่ครั้ง ผลลัพธ์ที่ได้เราจะได้เหมือนเดิม (โดยการเอาตัวแปร new_fruits ไปใช้งานต่อ)

จากที่กล่าวมาแล้ว ดังนั้นเมื่อไหร่ก็ตามที่มีโอกาสได้สร้าง data pipeline ก็อย่าลืมทำให้มีคุณสมบัติ idemponent กันด้วย

ผลลัพธ์ของงานควรจะเป็น Deterministic

เราสามารถกล่าวได้ว่า งานของเราสามารถทำซ้ำได้ ถ้างานเหล่านั้นเป็น Deterministic แปลว่างานนั้นๆ จะคืนค่าผลลัพธ์เดิมเสมอ ถ้าเราใส่อินพุตค่าเดิม

ในการพัฒนา Data Pipeline ให้เก็บข้อมูลไว้ที่ Shared Storage เสมอ

ตอนที่เราจัดการข้อมูลต่างๆ ใน Data Pipeline มักจะมีการอ่านและเขียนข้อมูลจากงานต่างๆ ใน Data Pipeline อยู่เป็นประจำ และงานแต่ละงานก็มักจะแชร์ข้อมูลระหว่างกัน การที่เราเก็บไฟล์ไว้ที่ Local File System ก็เป็นวิธีหนึ่งที่สามารถทำได้ ในกรณีที่เราจัดการระบบภายในเครื่อง 1 เครื่อง

ซึ่งในทางปฏิบัติแล้วเรามักจะมีเครื่องอยู่หลายเครื่อง ระบบเป็นแบบ Distributed และมีตัว Worker หลายตัวมาทำงานร่วมกันอยู่ ทำให้ถ้าเราสั่งให้งานๆ หนึ่งเขียนไฟล์ลง Local File System ไว้ ก็จะมีความเป็นไปได้สูงว่างานอื่นๆ ที่รันอยู่คนละเครื่องจะไม่สามารถเข้าถึงไฟล์นั้นได้

วิธีที่ง่ายที่สุดในการแก้ปัญหานี้คือให้เราใช้ Shared Storage แทน ซึ่ง Worker แต่ละตัว งานแต่ละงานจะสามารถเข้าถึงไฟล์ที่ต่างคนต่างเขียนลงมาได้