NAS ตัวใหม่ที่เข้าถึงได้ตลอดเวลา

เมื่อ 4 ปีก่อน ผมเปลี่ยน NAS มาเป็น Zyxel NSA325v2 ซึ่งพอคิด ๆ ไป 4 ปีมันก็เร็วเหมือนกันแฮะ จริง ๆ แล้วเจ้า Zyxel+Arch Linux ที่ใช้เองก็ใช้งานได้ดีไม่งอแงอะไรครับ แต่ว่าเราเริ่มรู้สึกว่ามันไม่ค่อยจะพอใช้แล้ว

เมื่อสักปลายปีที่แล้วผมเริ่มศึกษา Unreal Engine 4 จริงจัง แล้วเจอตออยู่อย่างคือขนาดโปรเจค โปรเจคของ UE ที่ผมทำอยู่มีขนาดราว ๆ 20-30GB ซึ่งถ้าไปใช้บริการฟรีอย่าง Github หรือ Bitbucket แล้วจะเจอแคปขนาดโปรเจคที่ 2GB เท่านั้น ถ้าอยากได้พื้นที่มากกว่านี้ก็ต้องจ่ายเพิ่ม แล้วพอจ่ายเพิ่มเองก็ได้แค่ราว ๆ 50GB เท่านั้น ก็ขึ้นอยู่กับเวลาเท่านั้นเองว่าขนาดโปรเจคมันจะไปได้ขนาดนั้นเมื่อไหร่

ผมก็เลยลองมองหาออปชันอื่นที่น่าจะทำให้เราทำงานได้ง่ายกว่า ก็พบว่า เอ๊ะแล้วถ้าเราใช้ NAS เราเป็น VCS ล่ะ NAS ตัวเก่าที่ผมใช้มีขนาดถึง 4TB ดังนั้นจะเอามาใช้เพื่อการนี้ย่อมไม่มีปัญหาอยู่แล้ว แต่ปัญหาอยู่ที่จะเข้าถึงได้อย่างไรมากกว่า

เข้าถึงได้ตลอดเวลา ผ่าน Zerotier

ออปชันแรกที่ผมดูคือการใช้ Dynamic DNS แต่เนื่องจากเนตเวิร์คที่บ้านเนี่ย IPv4 อยู่หลัง NAT เลยไม่สามารถเข้าถึงได้จากเนตเวิร์คส่วนใหญ่ครับ ดังนั้นออปชันนี้ก็เลยตกไป (ซึ่งจริง ๆ ก็ดีนะ เพราะวิธีนี้ไม่ค่อยปลอดภัยเท่าไหร่)

วิธีที่สองที่ดูคือการใช้ VPN แทน ซึ่งอันนี้โชคดีอย่างนึง คือมีคนสอนมาว่ามี service ที่ชื่อว่า Zerotier ซึ่งให้บริการ VPN ฟรี ตัวซอฟต์แวร์เป็น Open Source และมีการเข้ารหัสตลอดทาง สิ่งที่เราต้องทำคือสมัครเป็นสมชิกของบริการนี้ ติดตั้ง Client ที่ทุกเครื่องที่ต้องการเข้าถึง VPN สร้าง Network ในระบบขึ้นมาแล้วก็ Join ซะ (รายละเอียดจะไม่พูดถึงนะครับตรงนี้)

ปัญหาคือ ไอ้เจ้า Client เนี่ย มันไม่รองรับ NAS ตัวที่ผมใช้ครับ …

NAS เครื่องใหม่…หรือเครื่องเก่า

ผมตัดสินใจ … หรือเรียกว่าตัดใจก็ได้มั้ง? ที่จะเปลี่ยน NAS ใหม่อีกครั้ง ทั้ง ๆ ที่เครื่องเก่าก็ยังใช้งานได้ดี คราวนี้ไอเดียคือ เราจะให้มันรัน service มากกว่าแค่เป็น NAS และตัวดาวน์โหลด BitTorrent เจ้า NAS ตัวใหม่นี่จะเป็นตัวที่ดูแลเรื่องการพัฒนาซอฟต์แวร์และสามารถรันซอฟต์แวร์ที่เราเขียนขึ้นเพื่อใช้เองได้ด้วย ดังนั้นมันจะต้องเป็นเครื่องที่แรงพอสมควร

ในตอนแรกก็คิดว่าจะประกอบคอมขึ้นมาเพื่อการนี้โดยเฉพาะ แต่พอคิดเรื่องจำนวนเงินที่ต้องจ่าย … ผมก็ถอยไปออปชันที่สอง ก็คือไปเอาคอมเก่าของพี่ชายมาใช้ (พี่ชายผมทุกวันนี้ใช้แลปท็อปและไม่ค่อยเล่นเกมคอมแล้ว) แต่พอได้มาเสร็จเมนบอร์ดดันมาเสีย ก็เลยต้องเปลี่ยนเมนบอร์ด ผมมีซีพียูเก่าอยู่ (เป็น Core i5 4460) ก็เอามาใช้กับคอมเครื่องนี้

HDD เปลี่ยนใหม่ทั้งหมด เป็น WD RED 4TB x2 กับ Seagate Ironwolf 4TB สามตัวนี้เอามาต่อกันเป็น Software RAID5 ความจุรวม 8TB (ผมยังมี 2TB อีกสามตัว แต่ยังไม่ได้ใส่ เพราะว่าเคสไม่มีที่แล้ว)

Arch Linux OS เพื่อนเก่า

ตอนแรกเลยก็คิดว่า จะไปทาง NAS OS เต็มตัว ก็เลยติดตั้ง FreeNAS ไป แต่สุดท้ายก็ไม่ชอบ มันมีปัญหาอย่างนึงคือทุก Service ที่เราติดตั้งไปมันกลายเป็น VM ตัวใหม่ ดังนั้นถ้าเราจะใช้ Zerotier เราก็ต้องตามไปติดตั้งมันทุก VM (หรือตั้งค่าให้มัน route ผ่าน VM เครื่องนึงที่รัน Zerotier) จริง ๆ แล้วชอบ Web UI มันนะครับ ดูเหมือนทำงานด้วยสะดวก แต่มันไม่เข้ากับวิธีที่เราใช้ มันก็เลยต้องจากไป

ตัวเลือกต่อไปก็เลยเป็นตัวเลือกเก่าที่เราใช้บริการมานาน นั่นคือ Arch Linux ซึ่งสำหรับคนที่เคยใช้จะรู้ว่าการติดตั้งให้มันทำงานได้นั้นต้องใช้เวลาและความอดทนระดับนึง (เพราะต้องคอนฟิกทุกอย่างที่เราใช้) แต่พอผ่านจุดนึงไปแล้วเราแทบไม่ต้องไปยุ่งอะไรกับมันอีก

Service เดิมที่เคยใช้

Service เดิมที่ผมใช้ประจำ ก็คือ Samba กับ Transmission ซึ่งพอร่วมกับเครื่องที่สเปคสูงขึ้น มันก็รันเร็วขึ้นมากครับ

Samba ตอนนี้สามารถก็อปปี้ไฟล์ได้ด้วยความเร็วเฉี่ยว ๆ 100MB/s ซึ่งก็แทบจะติดเพดานของ Gigabit Ethernet แล้ว ส่วนตัว Transmission เองก็สามารถรองรับ torrent file ได้มากขึ้น ไม่มีอาการดีเลย์ตอนเปิดเครื่องละ

อ้อ ผมใช้ Nginx เป็น Web Server ซึ่งก่อนหน้านี้แทบไม่ได้ใช้อะไรจริงจัง แต่คราวนี้มันจะรับบทเป็นพระเอกในฐานะ Reverse Proxy Server ครับ

Service ใหม่ กับการรองรับกับการพัฒนาซอฟต์แวร์

สำหรับ Service ใหม่ ๆ ตัวแรกที่ผมติดตั้งก่อนเพื่อนคือ Gitea ตัวนี้เป็น Git Hosting Service ที่หน้าตาแทบจะลอก Github มาเลย (ฟีเจอร์ก็ใกล้เคียงกัน) ตัวนี้เขียนบน Go ครับ

Gitea in action

ตัวที่สองคือ Jenkins คือดู CI Server อยู่หลายตัว แล้วก็พบว่า สุดท้ายแล้วก็หนี Industry Standard ไม่พ้น

Jenkins

Jenkins มีหน้าที่บิลด์โค๊ดทุกครั้งที่ผม push อะไรเข้าไปใน Gitea แล้วหลังจากนั้นก็จะ Deploy Code ไปรันบนเครื่องทันที … ฟังดูดีใช่ไหมครับ ปัญหาคือแล้วจะรันยังไงล่ะ ???

ตอนแรกคือลืมคิดไปเลย เพราะว่าถ้าจะให้ Jenkins มันติดตั้งโปรแกรมที่ผมเขียนลงในเครื่องเนี่ย มันต้องมีสิทธิเทียบเท่า Root แล้วผมอ่านวิธี Deploy มันแล้วปวดหมองมาก (ฮา) ผมก็เลยไปเลือกใช้อีกออปชันนึงแทน นั่นคือ Docker เพราะว่ามันอยู่นอกเหนือตัว OS ระบบดังนั้น เราก็เลยไม่ต้องใช้สิทธิระดับ Root (ถึงแม้ว่า docker users จะมีสิทธิค่อนข้างสูงก็ตาม) ก็สามารถติดตั้งได้เหมือนกัน แล้วตอนรันก็แค่เรียก docker run เท่านั้นเอง

ซึ่งตอนนี้มีโปรแกรมนึงที่ผมเขียนด้วย go แล้วสร้างเป็น docker image เก็บไว้ในเครื่อง เป็นโปรแกรมที่จะดึง rss feed มาอ่านแล้วไปเรียก transmission เพื่อเพิ่มไฟล์ torrent ใหม่เข้าไปโดยที่ผมไม่ต้องมานั่งกดเอง โปรแกรมนี้จะตั้งให้รันหลังจากบูทเครื่องแล้ว 15 นาที และรันซ้ำทุกวันที่เวลาเดียวกัน การตั้งค่าให้รันตามเวลานี้ตั้งผ่าน systemd timer ครับ

ส่วนโปรแกรมอื่นเดี๋ยวคงตามมา ผมเริ่มทดลองสร้างอะไรแปลก ๆ ให้รันบนเครื่องนี้บ้างแล้วเหมือนกัน (หลัก ๆ ใช้ Go และ .Net Core) เอาไว้เดี๋ยวเอารูปมาโชว์ครับ แน่นอนว่าผมใช้คนเดียวไม่แบ่งใคร (ฮา)

Performance นอกบ้าน

อันนี้เป็นประเด็นหลักที่ย้ายเครื่องเลย (ฮา) ก็เลยต้องพูดถึงสักหน่อย

  • การก็อปปี้ไฟล์เข้าออกผ่าน Samba ก็ทำได้ถึงราว ๆ 30MB/s ครับ (ขึ้นกับเนตเวิร์คที่เราใช้ด้วย) ผมว่าไม่เลวนะ
  • การ clone/push-pull git อันนีได้ไม่สูงเท่า ก็ได้ราว ๆ 10MB/s (ขึ้นกับเนตเวิร์คเช่นเคย) ถ้าไม่ใช่โปรเจคใหญ่ก็ไม่มีปัญหามั้ง แต่ถ้าใหญ่หน่อยก็อาจจะต้องรอครับ แต่มันไม่ตัดนะ

ผมว่าความเร็วประมาณนี้ถือว่าโอเคนะ คือมันไม่เร็วเท่า Hosting แต่ราคาถูกกว่าแน่ ๆ

ปัญหาที่เจอ

มีปัญหาบ้างเล็กน้อย คือเหมือน Router จะมีปัญหาเวลารันไปนาน ๆ มันดันไปบล็อก zerotier เฉยเลย ก็รีสตาร์ท router ใหม่เท่านั้นเอง

อ้อ ค่าไฟน่าจะเพิ่มขึ้นพอสมควรครับ ห้องอาจจะร้อนบ้าง อันนี้ก็ต้องดูนิดนึง

สรุป

สรุปคือ เนื่องจาก NAS เก่าไม่ตอบโจทย์ความต้องการที่เปลี่ยนไป ผมก็เลยสร้างเครื่องใหม่ขึ้นมาแทน โดยมีสเปคโดยรวมสูงกว่ามาก นอกจากนี้ผมใช้ Zerotier เพื่อเข้าถึงบริการต่าง ๆ จากนอกบ้าน ผ่านระบบเครือข่ายเสมือน (VPN) ทำให้ไม่ต้องใช้ public ip และมีความปลอดภัยมากกว่าเพราะว่าไม่ได้เปิดให้คนภายนอกเข้าถึงได้ ความเร็วที่ใช้จากภายนอกก็ถือว่าไม่ได้เลวร้ายครับ ใช้ได้โอเคเลย

ผมติดตั้งซอฟต์แวร์ที่ใช้กับการพัฒนาซอฟต์แวร์มากขึ้น เป็นวง CI/CD ที่สมบูรณ์สำหรับซอฟต์แวร์เขียนใช้เองบางตัว แล้วตัว git ก็ใช้งานได้ดีกับเงื่อนไขที่เรามี ก็เลยไม่ต้องใช้ github ละ

ข้อเสียก็มีบ้าง ปัญหาก็มีบ้าง แต่โดยรวมกับสองสามเดือนที่ผ่านมาก็ถือว่าแฮปปี้ครับ

Singleton ไม่ดีจริงเหรอ ?

อันนี้คือ เท่าที่อ่านๆ บนเน็ต มีแต่คนบอกว่า “มาเลิกใช้ Singleton กันเถอะ” ซึ่งอันที่จริงสำหรับคนที่มีประสพการณ์พอสมควรจะรู้ว่า ปริมาณของ Singleton เนี่ยเป็นตัวบอกถึงว่าโค๊ดเรากำลังจะมีปัญหา แต่ว่ามันถึงขั้นต้องส่งโค๊ดไปถ้ำกระบอกเพื่อเลิกการเสพย์ติด Singleton เลยงั้นเชียวหรือ

บางคนบอกว่ามันเป็น Anti-Pattern ด้วยซ้ำ (มันคืออะไรเหรอครับคำนี้ :o)

Singleton คืออะไร

อ้างอิงจากหนังสือ Design Patterns, Elements of Reusable Object-Oriented Software (GoF) ซึ่งถือว่าเป็นหนังสือคลาสสิคของ Design Pattern กล่าวไว้ว่า

Intent Ensure a class only has one instance, and provide a global point of access to it.

เจตนาของแพทเทิร์นนี้ คือ ทำให้มั่นใจว่าคลาสนี้มีแค่หนึ่ง instance เท่านั้น และมอบวิธีการเข้าถึงไปหา instance นั้นโดยตรง

ซึ่งสาระสำคัญอยู่ตรงที่ 1 instance ครับ

Implementation

ในหนังสือเขาจะเขียนว่า คลาสที่เป็น Singleton นั้นจะอารมณ์ประมาณนี้

class Singleton {
public:
  static Singleton* Instance();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

นี่ลอกมาเป๊ะ ๆ เลย

โดยในหนังสือเขาให้ตัวอย่างว่า ไอ้ฟังก์ชัน Singleton::Instance() จะมีหน้าตาประมาณนี้

Singleton* Singleton::Instance() {
  if(_instance == nullptr) {
    _instance = new Singleton;
  }
  return _instance;
}

ผมแก้นิดนึงให้เข้ากับ ศตวรรษที่ 21 หน่อย

ทีนี้ การทำ Lazy-Initialization ซึ่งเป็น Implicit Initialization กล่าวคือ ไอ้ตัวแปร _instance เนี่ยจะเป็น null มาตั้งแต่ต้น และไม่เคยถูก Init ขึ้นมาเลยจนกระทั่งฟังก์ชัน Instance() ถูกเรียกเป็นครั้งแรก ซึ่ง ผมมองว่าเป็นหลุมพรางที่หลาย ๆ คนตกลงไป

ถ้าอ่านดี ๆ ในบทนี้ หนังสือเขาเขียนแค่ว่า นี่เป็น Common-Implementation ไม่ใช่ The only implementation กล่าวคือ ตราบใดที่คลาสนี้มีแค่ instance เดียว จะเขียนแบบไหนก็เขียนได้ครับ

กรณีของ Singleton ผมจะชอบทำ Explicit Initialization มากกว่า คือแทนที่จะบอกว่า ใช้ได้เลย เดี๋ยวถ้า instance ยังไม่พร้อมเดี๋ยวสร้างใหม่เอง ก็เป็นว่าให้ user คุมไปเลยว่าจะสร้าง instance เมื่อไหร่ แล้วเราก็สามารถทำ Explicit Clean-up ได้ด้วย

ก็อารมณ์แบบนี้ครับ

class Singleton {
public:
  static Singleton* Instance() {return _instance;};
  static void Init();
  static void CleanUp();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

มันก็แค่ฟังก์ชันอีกสองฟังก์ชันเท่านั้นเอง ว่าไหมครับ ถึงจุดนี้ผมก็ยังไม่ได้ละเมิดข้อตกลงที่ว่า ต้องมีแค่ instance เดียวนะ แค่บอกเฉย ๆ ว่ามันถูกสร้างเมื่อไหร่ และถูกทำลายเมื่อไหร่ เท่านั้นเอง

อ้อ ถ้า user ดันไปเรียกใช้ instance ก่อน init ก็ access-violation พังไปครับ อันนี้เรียกเสร่อ (ฮา) แต่ถ้าปราณีกันจะขว้าง exception กลับไปก็ดีเหมือนกัน

Instance ที่มันคืนมา ก็ไม่ได้จำเป็นว่าจะต้องมี type เดียวกับตัวคลาสด้วยซ้ำ

อันนี้เป็นอีกข้อนึงที่คนอ่านข้าม ทั้ง ๆ ที่มันก็อยู่หน้าข้าง ๆ ไอ้โค๊ดที่ผมก็อปมาเนี่ยแหละครับ

อย่างสมมติผมมีคลาส Singleton อันนึงชื่อ WindowManager (ลักษณะของคลาส Singleton อันนึงคือ มันชอบเป็น ผู้จัดการครับ ไม่รู้ทำไม) ผมสามารถสร้างโค๊ดในลักษณะนี้ได้ครับ

class WindowManager {
public:
  static WindowManager* Instance() {return _instance;};
  template<class T>
  static void Init();
  static void CleanUp();

protected:
  WindowManager();
private:
  static WindowManager* _instance;
};

class OSXWindowManager: public WindowManager {
//...
};

class LinuxWindowManager: public WindowManager {
//...
};

template<class T>
static void WindowManager::Init() {
  _instance = new T;
}

สังเกตตรงฟังก์ชัน WindowManager::Init() ที่เป็นเทมเพลตนะครับ คือตรงนี้เนี่ยเราระบุ type ตอน Init แบบ ให้ user เลือกเลยว่าอยากได้ class ไหน แต่เวลาเข้าถึงนี่เข้าได้จากที่เดียวนะ คือจาก WindowManager

ถ้าอ่านโค๊ดจากในหนังสือจะเห็นว่า ที่เขาวาง constructor ไว้เป็น protected ก็มีสาเหตุเพราะจะให้เราเลือกที่จะสร้าง subclass ก็ได้นะนี่แหละครับ แต่ก็มีข้อแม้ตรงที่ตัว subclass เองก็ควรเป็น protected ตามนะ ไม่งั้นเดี๋ยว user จะสร้าง instance เพิ่มได้ จะกลายเป็นละเมิดการออกแบบไป

หลายๆ คนน่าจะเห็นแล้วว่า โค๊ด WindowManager ข้างบนเนี่ย ดูแล้วเหมือนเป็นสองแพทเทิร์นรวมกัน ใช่ครับเราสามารถใช้ Singleton ร่วมกับ Abstract Factory ได้ด้วยครับ

Singleton เป็นตัวขวาง Dependency Injection??

จากการอ่านบนอินเตอร์เนทมา ผมพบว่ามีคนพูดถึงอันนี้เยอะอยู่ครับ คือเค้าว่ามันใช้คู่กับ DI ไม่ได้

แต่เอ ผมว่าผมเห็น Framework ที่ดังด้าน DI ตัวนึงอย่าง Spring เนี่ย ใช้ Singleton เพียบเลยนะ ?

ความเห็นส่วนตัวนะครับ คือเรามักจะเห็น DI ในท่าที่ว่า

result = state.Function(operatorObject, parameter);

แต่ถ้าเราใช้วิธีเปลี่ยน type ของ operatorObject ก่อนเรียก Function ล่ะ ?

OperatorClass.Init<TestOperator>();
result = state.Function(parameter);
OperatorClass.CleanUp();

เนี่ย ก็เป็นการ inject แบบอ้อม ๆ แล้วหรือเปล่า ?

แน่นอนว่าวิธีนี้ไม่เหมาะกับการใช้ในโค๊ดจริง แต่เป็นประโยชน์มากตอนทำ Unit Test ครับ

หรือจะเขียนแบบ

result = state.Function(OperatorClass.Instance(), parameter);

ก็ได้ ก็ดูตามสถานการณ์ อย่างกรณีหลังเนี่ยคือบางที OperatorClass อื่น ๆ อาจจะไม่ต้องเป็น Singleton แต่เผอิญว่าไอ้คลาสนี้จำเป็น อะไรแบบนี้

อีกอย่างคือ เราใช้ singleton ในการ implement ตัว state ก็ได้เหมือนกันครับ ก็จะเหมือนกับท่าปรกติทั่วไปนี่แหละ

แล้วปัญหาจริง ๆ มันคืออะไรกันแน่ ?

ส่วนตัวผมพบว่า Singleton เนี่ย คือ … มันมักจะถูกใช้ผิดที่น่ะครับ ตามนิยามของ Singleton เนี่ย คือ การออกแบบคลาสที่มีได้แค่ instance เดียว ซึ่งผมว่ามันเหมาะกับคลาสที่ต้องมี instance เดียว ไม่ใช่เราต้องการให้คลาสนี้มีแค่ instance เดียว(ต่างกันนะครับ) เช่นพวกคลาสที่เก็บ state ของ Hardware ในระบบ ส่วนตัวผมมักจะใช้สร้างตัว Renderer เพราะมันมักจะเป็นโค๊ดที่ ถ้าสร้าง instance ขึ้นมามากกว่าหนึ่งตัว โปรแกรมจะพัง (อันนี้ต้องเปิดเอกสาร API ประกอบครับ)

ถ้าเราต้องการให้มีแค่ instance เดียว แต่ถ้ามีมากกว่านั้นก็ไม่มีใครตาย ก็ไม่ต้องสร้างมันเป็น Singleton หรอกครับ สร้าง instance ไปแปะไว้สักคลาสนึงก็ได้ ไม่ต้องไปลิมิตว่ามันต้องมีแค่ 1 instance นะ อะไรแบบนี้

ที่เห็นคนใช้เยอะถึงเยอะมาก ๆ โดยไม่จำเป็น … คือ Logging System ครับ เอาจริง ๆ จะสร้างสักตัว Instance ตราบใดที่มันไม่เขียนลงไฟล์หรือ Device เดียวกันมันก็ไม่เจ๊ง ว่าไหมครับ ??

สรุป

Singleton เนี่ย เป็นคลาสที่ถูกออกแบบมาให้มีได้แค่ 1 instance นิยามมันมีแค่นี้ เราจะเล่นท่าไหนก็ได้ตราบใดที่ไม่ได้ละเมิดกฎข้อนี้ครับ และที่สำคัญคือเราไม่ควรใช้ถ้าไม่จำเป็น เมื่อมันจำเป็นมันจะเป็นแพทเทิร์นที่ทรงพลังมาก เพราะมันเป็นการป้องกันไม่ให้ user ไปสร้างของที่ไม่ควรสร้าง (instance ที่สองนั่นเอง)

ทั้งนี้ Dependency Injection นั้น จะใช้คู่กับ Singleton ก็ได้ ตรงนี้ขึ้นอยู่กับการออกแบบ Class Interface และ Function Interface ครับ และถึงแม้ว่าเรายังไปเรียก Instance()->DoSomething() ตรง ๆ (ซึ่งตามหลักแล้ว มันก็ไปละเมิด DI อยู่ดี) เรายังพอ Inject Instance ก่อนที่ฟังก์ชันนั้นจะถูกเรียกได้เหมือนกัน สิ่งที่สำคัญคือเราต้องรู้ว่ากำลังทำอะไรอยู่ครับ