ReuseLessSoftware - ใช้ซอฟต์แวร์ซ้ำให้น้อยลง
(wiki.alopex.li)- การโจมตีซัพพลายเชน กลายเป็นปัญหาใหญ่ขึ้น เพราะต้นทุนในการแจกจ่ายซอฟต์แวร์ต่ำมาก และมีการใช้งานระบบ build/การ deploy แบบอัตโนมัติอย่างแพร่หลาย
- ในยุค 1970 เคยมี วิกฤตซอฟต์แวร์ ที่ทำให้การสร้างซอฟต์แวร์ที่นำกลับมาใช้ซ้ำได้เป็นเรื่องยาก แต่ปัจจุบัน package repository และ package manager สามารถดึงโค้ดและ build ได้ด้วยแค่ชื่อกับเวอร์ชัน
- การอัปเดต dependency อัตโนมัติทำให้การเปลี่ยนแปลงที่เป็นอันตรายแพร่กระจายได้รวดเร็วผ่าน CI และการโจมตีซัพพลายเชนที่ดีจะแพร่ลามได้เร็วเท่ากับความเร็วที่ CI runner ทำงาน
- การ vendor dependency ทั้งหมดมาไว้ใน repository ของโปรเจ็กต์จะทำให้ repository ใหญ่ขึ้น แต่ช่วยกันการเปลี่ยนแปลงอัตโนมัติ และทำให้มองเห็นขนาดกับต้นทุนของ dependency ได้ชัดขึ้น
- แม้จะไม่ใช่คำตอบสำหรับซอฟต์แวร์ทุกประเภท แต่ซอฟต์แวร์ขนาดเล็กจำนวนมากอาจได้ประโยชน์จากการลด dependency ที่อาจเปลี่ยนจากภายนอกอย่างกะทันหันให้เหลือเพียง ระดับ 2~3 ตัว
ปัญหา
- การโจมตีซัพพลายเชนกลายเป็นปัญหาใหญ่ขึ้นเรื่อย ๆ ไม่ใช่เพราะธรรมชาติของซอฟต์แวร์หรือการบำรุงรักษาเปลี่ยนไปเท่านั้น แต่เพราะโมเดลต้นทุนของการแชร์และแจกจ่ายซอฟต์แวร์ต่ำลงมาก
- ต้นทุนการแจกจ่ายต่ำมากจนแม้จะมีความสิ้นเปลืองก็ยังมีการใช้อัตโนมัติอย่างหนัก ซึ่งตัวระบบอัตโนมัตินั้นก็มีประโยชน์
- ทุกไม่กี่เดือนจะมีการโจมตีซัพพลายเชนครั้งใหม่ที่ทำให้โค้ดจำนวนมากทั่วโลกเสียหาย
เรามาถึงจุดนี้ได้อย่างไร
- ช่วงปลายทศวรรษ 1960 ถึงต้นทศวรรษ 1970 ผู้คนยังไม่ค่อยรู้วิธีสร้างซอฟต์แวร์ที่นำกลับมาใช้ซ้ำได้ และเรียกสิ่งนี้ว่า วิกฤตซอฟต์แวร์
- ความต้องการซอฟต์แวร์เพิ่มขึ้นแบบเอ็กซ์โปเนนเชียล แต่ความสามารถในการสร้างซอฟต์แวร์ใหม่ให้ทันกับความซับซ้อนที่ต้องการเพิ่มขึ้นช้ากว่านั้น
- ช่วงเวลานี้นำไปสู่งานวิจัยด้าน modularity, structured programming และระบบโมดูลของภาษาโปรแกรมแทบทั้งหมดที่สร้างหลังปี 1990 ก็สามารถสืบสายย้อนกลับไปถึง Modula-2 ได้
- ในทศวรรษ 1990 และ 2000 อินเทอร์เน็ตได้สร้างทางออกที่ทรงพลังกว่า ทำให้การ build และแจกจ่ายซอฟต์แวร์มีต้นทุนต่ำลง และซอฟต์แวร์จำนวนมากที่ผู้คนอยากใช้จริงก็เป็นโอเพนซอร์ส
- จาก CPAN, CTAN และ Linux distribution ได้เกิด package repository และ package manager จำนวนมากขึ้นมา และเครื่องมือเหล่านี้ใช้เพียงไฟล์ manifest, ชื่อ และส่วนใหญ่คือเลขเวอร์ชันที่ค่อนข้างกำหนดกันเอง เพื่อค้นหา ดึง และ build ซอฟต์แวร์
-
จากการรวมระบบด้วยมือสู่ dependency อัตโนมัติ
- ในอดีต วิธีที่ดีในการสร้างระบบซอฟต์แวร์ซับซ้อนคือการนำชิ้นส่วนที่ใช้งานได้มาประกอบกันด้วยมืออย่างระมัดระวัง ซึ่ง Linux distribution ก็ทำสิ่งนี้เป็นหลัก
- ในปี 2003 การ build SDL พร้อมฟีเจอร์ทั้งหมดนั้นยุ่งยากถึงขั้นใช้เวลาหลายวัน และก็ไม่จำเป็นต้องโหยหายุคนั้น
- เมื่อมี Linux distribution เป็นสภาพแวดล้อมพื้นฐานที่รู้จักแน่ชัด ซอฟต์แวร์แบบปรับแต่งจำนวนมากก็สามารถทำงานในโลกของตัวเองได้ โดยไม่ต้องกังวลกับส่วนอื่นของระบบมากนัก
- เมื่อสื่อสารกับซอฟต์แวร์อื่น ก็มักทำผ่านไฟล์หรือ network socket ที่ใช้ protocol ที่รู้จักกันดี
- ปัจจุบันมีซอฟต์แวร์ดี ๆ จำนวนมากที่ build ตั้งแต่ต้นด้วย Rust หรือ Go หรือ deploy เป็น Docker container และซอฟต์แวร์เหล่านี้แทบไม่โต้ตอบกับ system library เลย
- แทนที่จะต้องปรับให้เข้ากับชุดซอฟต์แวร์ที่ OS distribution จัดมาให้ วิธีที่ระบบ build ดึงไลบรารีที่ต้องใช้มาเองได้กลายเป็นแนวทางที่แพร่หลาย
-
วิกฤตในทิศทางตรงกันข้าม
- ตอนนี้เรากลับเจอวิกฤตตรงข้ามกับยุค 1970 คือผู้คนใช้ซอฟต์แวร์ซ้ำมากเกินไปจนโปรแกรมแย่ลง
- การแจกจ่ายซอฟต์แวร์ยังคงราคาถูกมาก แต่การใช้งานซอฟต์แวร์ยังคงมีต้นทุน
- เป็นเวลานานที่ต้นทุนใหญ่ที่สุดคือความซับซ้อนของการ build ซอฟต์แวร์และทำให้มันรันได้บนคอมพิวเตอร์ แต่ปัญหานั้นส่วนใหญ่ถูกทำให้หายไปด้วยระบบอัตโนมัติแล้ว
- ตอนนี้เราจึง build, deploy และใช้ซอฟต์แวร์มากขึ้นมาก และต้นทุนก็ปรากฏในรูปของ dependency hell, ความพองตัว, เวลา build ที่ยาวนาน, และการหายไปของแพ็กเกจหรือ package manager
- ปัญหาที่ใหญ่ที่สุดคือ การโจมตีซัพพลายเชน
-
โครงสร้างการแพร่กระจายของการโจมตีซัพพลายเชน
- การโจมตีซัพพลายเชนเป็นปัญหาที่มีมานานพอ ๆ กับโอเพนซอร์สซอฟต์แวร์เอง
- ในอดีต เคยมีความพยายามส่งแพตช์อันตรายเข้า Linux kernel โดยใส่
uid = 0แทนuid == 0ซึ่งเป็นหนึ่งในความพยายามแก้ไขเคอร์เนลแบบประสงค์ร้ายที่ถูกพบจริงเป็นครั้งแรก และนับเป็นความพยายามโจมตีซัพพลายเชน - เหตุผลที่ในช่วง 10 ปีหลังการโจมตีซัพพลายเชนใหญ่ขึ้นและเป็นปัญหามากขึ้น คือระบบ build ถูกทำให้ดึง source code และนำไปแจกจ่ายโดยอัตโนมัติ
- ระบบ CI มักจะทำงานกับทุกการเปลี่ยนแปลงของโค้ดหรือการเปลี่ยนแปลงใหญ่ ๆ และการเปลี่ยนแปลงเหล่านี้ก็จะพร้อมให้ทุกคนที่พึ่งพาโค้ดนั้นใช้งานได้โดยอัตโนมัติ
- ระบบ CI ของฝั่งที่พึ่งพาก็จะดึงการเปลี่ยนแปลงนั้นมา รวมโค้ดอันตรายที่เพิ่งถูกเพิ่มเข้าไป และการโจมตีซัพพลายเชนที่ทำได้ดีจะลามเหมือนไฟป่าด้วยความเร็วเท่ากับที่ CI runner ทำงาน
- มีวิธีชะลอการโจมตีซัพพลายเชน เช่น dependency cooldown แต่ก็จะเกิดข้อถกเถียงเรื่องนโยบายและความรับผิดชอบ
ทางแก้
- แกนสำคัญคือไม่ปล่อยให้ระบบ build อย่าง
npm,cargoดึง dependency จากตำแหน่งบนเครือข่ายโดยอัตโนมัติทุกครั้ง แต่ให้เก็บ dependency ทั้งหมดไว้พร้อมกับซอฟต์แวร์ - ทำ vendor dependency ทั้งหมดเข้ามาในโปรเจ็กต์ แล้วคัดลอกเนื้อหาจาก upstream source control มาใส่ใน git repository และ commit ไว้
- เมื่อมีการอัปเดตจาก upstream ก็แค่ดาวน์โหลดมาแล้วคัดลอกเข้ามาใหม่ และถ้าการทำด้วยมือเริ่มน่าเบื่อก็ค่อยให้เครื่องมือ build ช่วยทำอัตโนมัติ
- ถ้ามี lockfile อยู่แล้ว ก็ทำให้มันผูกกับ source tree ทั้งชุดที่อยู่ใน source control ได้เลย
- เป็นการถือครองทุกบรรทัดของ source code ด้วยการควบคุมอย่างเข้มงวด
-
ต้นทุนและ trade-off
- repository จะใหญ่ขึ้น แต่พื้นที่ดิสก์มีราคาถูก
- ต้นทุนการส่งข้อมูลไม่ได้ถูกเท่าดิสก์ แต่ในบริบทนี้ยังเป็นสิ่งที่ต้องยอมรับ
- เวลา build อาจดูเหมือนจะเพิ่มขึ้น แต่เพราะยังไงก็ต้อง build dependency พวกนั้นใหม่อยู่แล้ว จึงไม่ได้เพิ่มขึ้นเสมอไป
- การนำโค้ดกลับมาใช้ซ้ำอาจยากขึ้น และอาจเป็นปัญหาจริงสำหรับโปรแกรมอย่าง client และ server ที่ใช้ไลบรารี protocol ร่วมกัน
- โปรแกรมแบบนั้นมีปัญหาเรื่องเวอร์ชันไม่ตรงกันอยู่แล้วและจำเป็นต้องรับมือ ดังนั้นการทำให้ต้องใส่ใจมากขึ้นจริง ๆ ก็ไม่ได้แย่กว่าเดิมในระยะยาว
-
แนวกันไฟของการโจมตีซัพพลายเชน
- หากไม่อัปเดต dependency โดยอัตโนมัติ ทุกแพ็กเกจใน ecosystem ก็จะกลายเป็น แนวกันไฟ สำหรับการโจมตีซัพพลายเชน
- วิธีเดียวกันนี้ยังขัดขวางการกระจายของ bug fix และแพตช์ด้วย แต่ถ้าเป็นการแก้ไขสำคัญ มนุษย์ก็มักต้องไปตามดูเองอยู่ดี
- การแก้ไขที่ไม่มีใครไปตามดู มักไม่ใช่เรื่องสำคัญมากนัก
- หากเลิกใช้แนวคิดเรื่อง semver หรือแนวคิดว่า “โค้ดสองชุดที่ต่างกันควรทำงานแบบเดียวกัน” ในระบบ build และมองหมายเลขเวอร์ชันทุกตัวเป็นเอกลักษณ์ที่ไม่เกี่ยวข้องกัน ก็อาจได้ผลคล้ายกัน
- ปัญหาของ semver คือมันสะท้อนเจตนาของคน ไม่ใช่ความจริงในโลกจริง และยังใช้ได้ก็ต่อเมื่อถูกใช้อย่างถูกต้องในระดับหนึ่งเท่านั้น
- การมองหมายเลขเวอร์ชันเป็นเอกลักษณ์แยกกัน ไม่ได้ช่วยแก้ปัญหา dependency หายไป ถูกแก้ไขแทรกแซง หรือเนื้อหาแพ็กเกจเสียหายในรูปแบบอื่น
-
การมองเห็น dependency
- การ vendor dependency ทั้งหมด นอกจากจะช่วยชะลอการเปลี่ยนแปลงอัตโนมัติแล้ว ยังเพิ่มต้นทุนของการใช้ dependency ขึ้นเล็กน้อย
- ต้นทุนที่เพิ่มขึ้นไม่ได้รุนแรงจนรับไม่ได้ แต่ทำให้ต้องคิดเพิ่มอีกนิดเวลาใช้โค้ดจาก upstream
- มันเป็นกลไกแบบนุ่มนวลที่ทำให้ถามตัวเองอีกครั้งว่า “จำเป็นจริงหรือไม่” ตอนจะเพิ่ม dependency ใหม่
- การมองเห็น ของ dependency สูงขึ้น และความพองตัวที่ซ่อนอยู่หลัง dependency ก็ซ่อนได้ยากขึ้น
- ถ้าเพิ่มไลบรารีง่าย ๆ ที่คิดว่าน่าจะมีแค่ราว 200 บรรทัด แต่จริง ๆ กลับมี 50,000 บรรทัด ก็จะชัดขึ้นว่าควรหยุดและถามเหตุผล
- ความรู้สึกมหัศจรรย์ของ dependency จะลดลง และทำให้ตามรอยเส้นทางที่บั๊กใน codebase เชื่อมไปสู่โค้ดของคนอื่นได้ง่ายขึ้น
-
ต้นไม้ dependency และปัญหาการแชร์
- ถ้า vendor ทุกอย่างเป็นค่าเริ่มต้น อาจนำไปสู่ต้นไม้ dependency ที่แบนและกว้างขึ้น
- การไปถึงระดับไลบรารียักษ์อย่าง Boost หรือ Qt ในโลก C++ ไม่ใช่สิ่งที่พึงปรารถนา
- ไลบรารียักษ์เหล่านั้นมีอยู่เพราะการสร้างและใช้งานไลบรารี C/C++ ขนาดเล็กนั้นยากเกินไป
- มีสมมติฐานว่าการให้ system integrator อย่าง Linux distribution ทำเรื่องการ build เหล่านี้ให้ครั้งเดียว จะดีกว่าการที่แต่ละคนต้องไปทำความเข้าใจวิธี build ของ Boost หรือ Qt เอง
- ข้อเสียจริงคือ dependency แบบ transitive จะไม่ถูกแชร์ร่วมกัน
- ถ้า lib A และ lib B ต่างก็พึ่งพา Z การ deduplicate ไม่ใช่ว่าทำไม่ได้ แต่จะยากขึ้น และต้องให้คนทำเองหรือใช้เครื่องมือที่ซับซ้อนขึ้น
- แม้ตอนที่ dependency แบบ transitive ถูกแชร์ร่วมกันก็ยังมีปัญหาอยู่ และการมี dependency แบบ transitive เองก็เป็นส่วนหนึ่งของปัญหา
- การอนุญาตให้ไลบรารีระบุ dependency แบบ transitive ได้ เท่ากับมอบการควบคุมโปรแกรมของเราให้คนอื่น
การวิเคราะห์
- ไม่ใช่ว่าซอฟต์แวร์ทุกชนิดจะใช้วิธีนี้ได้
- การ vendor และ build Redis ทั้งตัวเป็นส่วนหนึ่งของการ deploy เว็บแอป backend ไม่ใช่สิ่งที่สมเหตุสมผลเป็นพิเศษ
- อย่างไรก็ตาม ถ้าการ deploy ถูกทำให้เป็นอัตโนมัติด้วย Ansible หรือ Docker image อยู่แล้ว ก็มีโอกาสสูงว่าคุณกำลังทำสิ่งคล้ายกันอยู่โดยพฤตินัย
- วิธีนี้มีขีดจำกัดของความซับซ้อนที่รับไหว แต่บริษัทที่ใช้ monorepo ขนาดมหึมาอย่าง Google และ Facebook แสดงให้เห็นว่าขีดจำกัดนั้นอาจสูงกว่าที่คิด
- ถึงจุดหนึ่ง dependency ก็ต้องไปเจอกับระบบปฏิบัติการ และตัวระบบปฏิบัติการเองก็เป็น dependency ขนาดใหญ่ที่มีปัญหาในตัวมากมาย
- แนวคิด unikernel สำหรับเว็บแบ็กเอนด์ฟังดูน่าสนใจ แต่ในทางปฏิบัติยังมีปัญหาเรื่องเครื่องมือ และเรายังไปไม่ถึงจุดนั้น
-
Linux distribution และสภาพแวดล้อมการ build
- วิธีนี้ไม่ใช่แนวทางสำหรับสร้างระบบที่มีปฏิสัมพันธ์กันครบชุดแบบ Linux distribution หรือ BSD
- ระบบลักษณะนั้นมีโปรแกรมและไลบรารีจำนวนมากที่ต้องทำงานร่วมกัน จึงเป็นอีกปัญหาหนึ่ง
- หากผลักหลักการนี้ไปจนสุด ก็จะเข้าใกล้แนวทางแบบ Nix หรือ Guix
- แนวคิดที่ว่าต้องประกอบ “สภาพแวดล้อมการ build” ให้ถูกต้องนั้น ใกล้เคียงกับการแก้ปัญหา “จะ build ซอฟต์แวร์อย่างไร” แบบขี้เกียจและไม่เพียงพอ
- แนวคิดนี้เป็นเศษตกค้างจากยุคที่ build ซอฟต์แวร์บน minicomputer เครื่องหนึ่งครั้งเดียว แล้วแจกจ่ายเป็นไบนารีในวงกว้าง
- ทุกวันนี้เราทำการ build ซอฟต์แวร์สด ๆ มากกว่ายุค 1970 มาก
-
ขอบเขตที่นำไปใช้ได้
- วิธีนี้ไม่ใช่คำตอบสารพัดนึก แต่มีซอฟต์แวร์จำนวนมากที่นำไปใช้ได้และได้รับประโยชน์
- ซอฟต์แวร์ส่วนใหญ่มีขนาดเล็ก ส่วนโปรเจ็กต์ใหญ่ ๆ ก็มักต้องแก้ปัญหาแบบนี้อยู่แล้วจำนวนมาก
- มีไลบรารีมากมายที่ทำงานคำนวณล้วน ๆ หรือแตะโลกภายนอกผ่าน I/O พื้นฐานและพกพาได้ เช่นไฟล์และ network socket เท่านั้น
- ตัวอย่างอย่าง compression library, libcurl, TUI library, หรือ Django สามารถถือเป็นเป้าหมายของการ vendor ได้
- การ vendor ช่วยหลีกเลี่ยงปัญหาที่เวลา deploy หรือ build บนระบบใหม่แล้วทุกอย่างพังแบบหาสาเหตุไม่ได้ เพราะเวอร์ชันชนกันหรือมีบั๊กจากแพตช์ที่เข้ามากะทันหัน
- เป้าหมายคือการลด dependency ที่อาจเปลี่ยนจากภายนอกโดยไม่มีการแจ้งล่วงหน้า จากระดับ 200~300 ตัว ให้เหลืออย่างมากเพียง 2~3 ตัว
บทสรุป
- การลดการอัปเดต dependency อัตโนมัติ และให้โปรเจ็กต์ถือครอง source ของ dependency เอง จะช่วยชะลอการแพร่กระจายอัตโนมัติของการโจมตีซัพพลายเชนได้
- การเพิ่มต้นทุนของการใช้ dependency ขึ้นเล็กน้อยและเพิ่มการมองเห็น จะช่วยให้พบการใช้ซ้ำที่ไม่จำเป็นและความพองตัวที่ซ่อนอยู่ได้ง่ายขึ้น
- วิธีนี้ไม่เหมาะกับทุกระบบ แต่สำหรับซอฟต์แวร์ขนาดเล็กและไลบรารีจำนวนมาก มันมีข้อดีในทางปฏิบัติ
1 ความคิดเห็น
ความเห็นจาก Lobste.rs
ตัวจัดการแพ็กเกจของ Zig ดูเป็นทางประนีประนอมที่ค่อนข้างดี
ทุกแพ็กเกจถูกตรึงด้วย content hash เลยเท่ากับมี lock file โดยปริยาย จึงหลีกเลี่ยงปัญหาแบบ “รีโพต้นทางจู่ ๆ ก็กลายเป็นของอันตราย” ได้ แต่ปัญหา “รีโพต้นทางหายไป” ยังมีอยู่
แต่เพราะมีทั้งแคชแบบ global/local และอิงตาม content hash ถ้ารีโพต้นทางหายไป ก็แค่เอา tarball จากสำเนาในเครื่องไปวางไว้ตรงที่ต้องใช้
ดูเป็นจุดกึ่งกลางที่ดีระหว่าง “vendoring ซอร์ส” กับ “ซอฟต์แวร์ที่เรียบง่ายและนำกลับมาใช้ซ้ำได้”
เอาซอร์สทั้งหมดไว้ใน content-addressed store แล้วให้แต่ละโปรแกรมถูกแฮชจากแฮชของอินพุตที่ใช้
คงต้องแก้ lock file หรือไม่ก็หา hash collision ซึ่งทั้งสองอย่างก็ดูไม่ง่าย
แต่เพราะคุ้นกับ ecosystem ของ
cargoเลยยังไม่ชอบหมดใจนัก เวลาอัปเดต dependency ก็มักมีแนวโน้มที่ transitive dependency จะถูกอัปเดตตามไปเงียบ ๆ ด้วย และตัวอื่นที่อยู่ในช่วง semantic version ที่เข้ากันได้ก็จะเปลี่ยนตามไปด้วยจะเรียกว่า “การโจมตีซัพพลายเชน” ก็ดูไม่ใช่ เพราะไม่มีสัญญาที่ลงนามพร้อมข้อเสนอและสิ่งตอบแทน จึงไม่นับว่าเป็นซัพพลายเชน
แต่อีกมุมหนึ่ง ถ้าพูดถึงการรับประกันว่า dependency จะไม่เปลี่ยนจากข้างล่าง lock file ที่มี hash หรือแนวทาง minimal version selection ของ Go ก็เทียบได้กับการ vendor dependency
เข้าใจว่าการ vendoring มีแรงเสียดทานเพิ่มขึ้น แต่ถ้าสุดโต่งไปก็จะกลายเป็นต้องเขียนเอง หรือแย่กว่านั้นคือทำ dependency เป็นโค้ดที่สร้างแบบสด ๆ ดังนั้นผมคิดว่าการใช้ซอฟต์แวร์ที่ผู้เชี่ยวชาญในโดเมนเขียนและผ่านการตรวจสอบมาดีแล้วดีกว่า
ผมเคยทำงานด้านนี้ที่ Facebook และไม่อยากแนะนำการ จัดการ dependency ของ third-party ที่นั่นให้ใครเลย สำหรับ direct dependency ของ Rust crate ตัวหนึ่ง จะอนุญาตให้มีเวอร์ชันที่เข้ากันไม่ได้ตาม semantic version พร้อมกันได้มากสุดแค่สองเวอร์ชันใน fbsource ทั้งหมด ถ้าจะอัปเดต dependency คุณต้องแบกรับภาระการอัปเดตทั้ง fbsource ไปด้วย
มันอาจเหมาะกับ Facebook แต่ผมไม่คิดว่ามันยอดเยี่ยมหรือยั่งยืนเป็นพิเศษ
ผมสงสัยว่าที่ว่า “ไม่ได้ยอดเยี่ยมหรือยั่งยืนเป็นพิเศษ” น่าจะขึ้นกับขนาดมากกว่าตัวนโยบายเอง ถ้ายอมให้มีหลายเวอร์ชันก็จะเกิดปัญหาอีกแบบ เพราะภาษาสมัยใหม่ส่วนใหญ่ยกเว้น TypeScript ใช้ nominal typing เป็นหลักหรือเกือบทั้งหมด ดังนั้นทุกครั้งที่มี breaking change ก็จะนำ type ข้ามเวอร์ชันมาใช้ซ้ำไม่ได้ ถ้าไม่ใช้ “semver trick”
ตอน Log4Shell ผมจำได้ชัดว่าบริษัทที่มีหลายเวอร์ชันกระจัดกระจายอยู่หลายจุด อัปเกรดยากกว่าบริษัทที่มีจำนวนน้อยหรือปักเวอร์ชันไว้
ตาม The Third Networking Truth ที่ว่า “ด้วยแรงขับมากพอ หมูก็บินได้ แต่ไม่ได้แปลว่านั่นเป็นความคิดที่ดี”
แนวปฏิบัติจำนวนมากที่ถูกยกมาจาก Google/Facebook ใช้ได้ก็เพราะบริษัทเหล่านั้นทุ่ม แรงขับมากพอ ได้
อย่างเช่น ผมรู้ว่าบางแห่งตั้งทีมที่ใหญ่กว่าจำนวนพนักงานทั้งบริษัทของผมเพื่อรองรับ monorepo และตัวเลือกด้าน dependency แบบนั้น พวกเขารับไหว แต่พวกเราส่วนใหญ่รับไม่ไหว
มุมมองดีมาก ผมเห็นด้วยแรง ๆ กับประเด็นที่ว่า “ถ้า vendor dependency ทั้งหมด ต้นทุนของการใช้ dependency ก็จะสูงขึ้น”
แต่ก็ไม่ควรคัดลอก libcurl มาแปะตรง ๆ นะ สำหรับไลบรารีส่วนใหญ่ก็เป็นกลยุทธ์ที่โอเค แต่ไม่ใช่คำแนะนำที่ดีสำหรับโปรแกรม C ที่ต้องรับมือกับอินพุตที่เป็นปฏิปักษ์ ระบบปฏิบัติการไม่น่าจะมีใครดูแลให้ libcurl ปลอดภัยได้ดีกว่า
จุดหนึ่งที่ผมไม่เคยคิดมาก่อนคือ มันแปลกอยู่นิดหน่อยที่ตัวจัดการแพ็กเกจสำหรับผู้ใช้ปลายทางอย่าง apt มาก่อน แล้วตัวจัดการแพ็กเกจระดับภาษาค่อยตามมาทีหลัง
ผมคิดว่านี่ทำให้เกิดปัญหาจริงเยอะมาก ถ้าดู rubygems ช่วงต้นยุค 2000 จะเห็นค่อนข้างชัดว่ามันพยายามทำ “apt สำหรับ Ruby” โดยมีค่าเริ่มต้นเป็นการติดตั้งทั้งระบบ ไม่ใช่การจัดการรายโปรเจกต์ การย้อนแก้ผลเสียจากความผิดพลาดนั้นต้องใช้เวลาหลายสิบปีและต้องเพิ่ม bundler เข้ามา แต่ถ้ายอมรับตั้งแต่แรกว่าต้องมี การแยกระดับโปรเจกต์ ก็คงไม่ต้องมี bundler
Python ยังเก็บกวาดความสับสนนี้อยู่ และ Perl ก็น่าจะยังเหมือนกัน แต่ผมไม่ค่อยรู้รายละเอียด
ในเชิงประวัติศาสตร์ ตัวจัดการแพ็กเกจเดิมทีคือวิธี สร้างระบบ และระบบพวกนั้นมีผู้ใช้หลายคน มีเดสก์ท็อปหลายแบบ และมีซอฟต์แวร์จำนวนมากที่ต้องทำงานร่วมกัน
การ build ซอฟต์แวร์กินเวลาและหน่วยความจำมาก และเมื่อเทียบกับดิสก์กับ RAM แล้ว ซอฟต์แวร์ก็มีเยอะมาก การใช้ไลบรารีซ้ำจึงสำคัญ
เมื่อเว็บแอปเริ่มมีบทบาท คอมพิวเตอร์สำคัญส่วนใหญ่ก็กลายเป็นเซิร์ฟเวอร์ที่ตลอดอายุใช้งานรันโปรแกรมอยู่ไม่กี่ตัว และดิสก์กับ RAM ก็ถูกพอที่ขนาดไบนารีของโค้ดสำคัญน้อยลง
เครื่องมือสำหรับสร้างระบบตามการเปลี่ยนแปลงของยุคสมัยไม่ทันนัก ทำให้คนส่วนใหญ่ที่สร้างซอฟต์แวร์ต้องการแค่เครื่องมือที่ช่วยทำโปรแกรมเดี่ยวให้ดี ไม่ใช่ระบบเชื่อมโยงขนาดมหึมาที่มี shared library เต็มไปหมด
ควบคู่กับประวัติศาสตร์นี้ก็มีอีกสายหนึ่งคือ “C ไม่มีระบบโมดูลที่ดีพอ” แต่ที่นี่ไม่ค่อยสำคัญเท่าไร
ผมอาจจะเข้าใจผิด แต่การคัดลอก dependency มาไว้เองน่าจะมีข้อเสียคือ ถึงจะมีบั๊กอยู่ในนั้น สแกนเนอร์ก็ตรวจไม่เจอ
ถ้าอย่างนั้นปัญหาที่ตามปกติควรได้รับการแจ้งเตือนอาจค้างอยู่เงียบ ๆ
สแกนเนอร์มีประโยชน์มากในการชี้ว่าอะไร “อาจ” เป็นปัญหา แต่เวลามันทำให้คุณต้องเลื่อนงานที่วางแผนไว้กะทันหันเพื่อไปแก้สิ่งที่มันคิดว่าเป็นปัญหาแต่จริง ๆ ไม่ใช่ นั่นแหละที่ปวดหัวมาก
ถ้าทำตามข้อเสนอ คือรวม dependency ทั้งหมดเข้าไปในซอฟต์แวร์ คัดลอกการจัดการซอร์สต้นทางมาใส่ในรีโพ git แล้ว commit ไว้ และถ้าเบื่องานมือก็ให้เครื่องมือ build ทำให้อัตโนมัติ สุดท้ายมันก็วนกลับมาเป็นการรวม ซอฟต์แวร์ third-party เข้ามาโดยที่ไม่ได้ดูมันจริง ๆ อยู่ดีไม่ใช่เหรอ?
แต่แนวทางนั้นก็ยังไม่แก้ปัญหา dependency หายหรือถูกแก้ไข หรือปัญหาที่ใครบางคนไปยุ่งกับเนื้อหาแพ็กเกจด้วยวิธีอื่น มันใกล้เคียงกับการปรับแต่งประสิทธิภาพมากกว่า และในความเห็นผมเป็นการปรับแต่งก่อนเวลาอันควร วันหนึ่งอาจไปถึงจุดนั้นได้ แต่ไม่ควรใช้เป็นจุดตั้งต้น