อย่าทำให้โค้ดนี้เรียบง่ายขึ้น
(github.com/kubernetes)pv_controller.goของ Kubernetes เป็นคอนโทรลเลอร์ที่ซิงโครไนซ์ การ bind ระหว่าง PV/PVC และตั้งแต่คอมเมนต์ต้นไฟล์ก็ระบุชัดว่า “อย่าทำให้เรียบง่ายขึ้น ให้คงสไตล์ space shuttle ไว้”- สไตล์นี้คือการมี
elseคู่กับifทุกตัว และใส่คอมเมนต์ไว้แม้กับเงื่อนไขที่ดูชัดเจน เพื่อแสดง สาขาที่ผ่านการพิจารณาแล้วและเจตนา ไว้ในโค้ด - แกนของการออกแบบคือ pointer สองทาง ระหว่าง
pvc.Spec.VolumeNameและpv.Spec.ClaimRefซึ่งช่วยให้รับมือกับการแข่งขัน การลบ การแก้ไขโดยผู้ใช้ และการ bind พร้อมกันได้แบบกู้คืนได้ในสภาพแวดล้อมที่ไม่มี transaction - คอนโทรลเลอร์ผูกการเฝ้าดูการเปลี่ยนแปลงของ PV/PVC, cache ภายใน, worker queue เดี่ยว, การบันทึก event, dynamic provisioning และอินเทอร์เฟซ CSI migration เข้าด้วยกัน เพื่อจัดการ การเปลี่ยนสถานะของการ bind
- สาขาเงื่อนไขและคอมเมนต์ที่ยืดยาวเป็นกลไกสำหรับรักษาความรู้เชิงธุรกิจของพฤติกรรมและบริบทการกู้คืนจากความล้มเหลว ดังนั้นการเปลี่ยนแปลงในอนาคตก็ควรทำตามสไตล์เดียวกัน
บทบาทและหลักการเขียนของ pv_controller.go
pv_controller.goเป็นไฟล์ implementation ของ PersistentVolumeController ในแพ็กเกจpersistentvolumeของ Kubernetes- คอนโทรลเลอร์นี้ทำให้สถานะของ
PersistentVolumeClaimและPersistentVolumeสอดคล้องกัน- cache controller ที่เฝ้าดูการเปลี่ยนแปลงของ
PersistentVolume - cache controller ที่เฝ้าดูการเปลี่ยนแปลงของ
PersistentVolumeClaim - ซิงโครไนซ์สถานะ PV/PVC จาก event การเปลี่ยนแปลงของอ็อบเจ็กต์ทั้งสอง
- cache controller ที่เฝ้าดูการเปลี่ยนแปลงของ
- คอมเมนต์ต้นไฟล์เตือนซ้ำ ๆ ว่า อย่าทำให้โค้ดนี้เรียบง่ายขึ้น
- ชื่อสไตล์คือ
space shuttle style - เป็นแนวทางที่มี
elseคู่กับคำสั่งifทุกตัว - เป้าหมายคือระบุทุกสาขาให้ชัดเจน ยกเว้นการเช็ก error แบบง่าย ๆ
- แม้พฤติกรรมที่ดูชัดเจนก็เขียนเป็นคอมเมนต์ เพื่อให้ผู้ดูแลรักษาตามความซับซ้อนของการ bind ได้
- ชื่อสไตล์คือ
เหตุผลที่ต้องคง space shuttle style
- เดิมทีคอนโทรลเลอร์นี้เป็นผลจากการรวมงานที่เคยแยกอยู่ในคอนโทรลเลอร์สามตัวให้มาอยู่ตัวเดียว
- ในกระบวนการทำให้ subsystem ของ PV เรียบง่ายขึ้น จำเป็นต้องมีวิธีที่จัดการทุกเงื่อนไขอย่างชัดเจนในโค้ด
- ผลลัพธ์คือโค้ดอาจดูยืดยาวและมีคอมเมนต์กับสาขาเงื่อนไขจำนวนมาก
- ความยืดยาวนี้เป็นกลไกสำหรับทิ้ง ความรู้เชิงธุรกิจและบริบทของพฤติกรรมการ bind ไว้ในโค้ด
- เมื่อแก้ไฟล์นี้ต้องรักษา
space shuttle styleไว้ และหากจำเป็นให้เพิ่มสาขาและคอมเมนต์ด้วยแนวทางเดียวกัน
การออกแบบหลัก: pointer สองทางของ PV และ PVC
- แกนของการออกแบบคือ pointer สองทาง ระหว่าง PV กับ PVC
- pointer ฝั่ง PVC:
pvc.Spec.VolumeName - pointer ฝั่ง PV:
pv.Spec.ClaimRef
- pointer ฝั่ง PVC:
- ความเป็นสองทางนี้จัดการได้ยากในระบบที่ไม่มี transaction แต่จำเป็นเพื่อรับประกันการทำงานที่ถูกต้องแม้เกิดความขัดข้อง
- หาก rogue HA controller instance สร้าง race condition อาจเกิดการ bind หลายรายการที่แยกไม่ออก และทำให้มี ความเป็นไปได้ที่ข้อมูลจะสูญหาย
- โดยพื้นฐานแล้วคอนโทรลเลอร์ออกแบบให้ทำงานในโหมด high availability แบบ active-passive
- การเปลี่ยนผ่านของอ็อบเจ็กต์ถูกออกแบบให้ทำงานได้แม้ใน active-active HA
- อย่างไรก็ตาม หากคอนโทรลเลอร์ active สองตัวชนกันบ่อย ประสิทธิภาพอาจลดลงได้
วิธี bind และเงื่อนไขการกู้คืน
- คอนโทรลเลอร์รองรับ อ็อบเจ็กต์ pre-bound แบบสองทาง
- PVC ที่ต้องการ PV เฉพาะ
- PV ที่ถูกจองไว้สำหรับ PVC เฉพาะ
- การ bind ดำเนินเป็นสองขั้นตอน
- แก้ไข
PV.Spec.ClaimRefก่อน - จากนั้นแก้ไข
PVC.Spec.VolumeName
- แก้ไข
- ในทุกจุดของกระบวนการนี้ PV หรือ PVC อาจถูกผู้ใช้หรือคอนโทรลเลอร์อื่นแก้ไขหรือลบได้
- คอนโทรลเลอร์ตั้งแต่สองตัวขึ้นไปอาจพยายาม bind volume และ claim ต่างชุดพร้อมกันก็ได้
- คอนโทรลเลอร์ต้องสามารถ กู้คืน จากสถานการณ์ขัดแย้งเหล่านี้ได้
องค์ประกอบหลักของ struct คอนโทรลเลอร์
PersistentVolumeControllerมี lister, ฟังก์ชัน sync ของ informer, Kubernetes client, event recorder, volume plugin manager และส่วนอื่น ๆ ที่จำเป็นต่อการซิงโครไนซ์ PV/PVC- เวอร์ชันล่าสุดที่ทราบของ PV/PVC ถูกเก็บใน cache ภายใน
volumes persistentVolumeOrderedIndexclaims cache.Store
- cache นี้สะท้อนทั้งเวอร์ชันล่าสุดที่บันทึกไว้ใน API server และเวอร์ชันที่เข้ามาผ่าน event ของ etcd
- การ bind หนึ่งครั้งอาจสร้าง event ได้ประมาณสี่รายการ
- อัปเดต
volume.Spec - อัปเดต
volume.Status - อัปเดต
claim.Spec - อัปเดต
claim.Status
- อัปเดต
- หากไม่มี cache ภายใน เมื่อ informer ถือสถานะเก่าอยู่ อาจพยายามแก้การ bind ที่เสร็จแล้วซ้ำ
- เมื่อพยายามเขียนกลับไปยัง API server ในจังหวะนั้น อาจเกิด version conflict กับอ็อบเจ็กต์ที่บันทึกไว้แล้ว
Work queue และข้อจำกัดด้าน concurrency
- คอนโทรลเลอร์มี workqueue แยกสำหรับประมวลผล claim และ volume
claimQueuevolumeQueue
- แต่ละ queue ต้องมี worker thread เพียงหนึ่งตัวเท่านั้น
- โดยเฉพาะ
syncClaim()ไม่สามารถ re-enter ได้ - หาก
syncClaim()สองตัวทำงานพร้อมกัน อาจเกิดปัญหาต่อไปนี้- bind claim สองรายการต่างกันเข้ากับ volume เดียวกัน
- bind claim หนึ่งรายการเข้ากับ volume สองตัว
- คอนโทรลเลอร์สามารถกู้คืนสถานการณ์เหล่านี้ได้ด้วย version error จาก API server และการตรวจสอบของตัวเอง แต่แนวทาง multi-worker อาจทำให้ความเร็วโดยรวมลดลง
syncClaim: จุดเข้าของการซิงโครไนซ์ PVC
syncClaimเป็นเมธอดหลักที่ถูกเรียกเมื่อ claim ถูกสร้าง อัปเดต หรือซิงโครไนซ์ตามรอบเวลา- เมธอดนี้ไม่แยกประเภท event
- ขั้นแรกจะตั้ง migration annotation ที่ถูกต้องให้ PVC และอัปเดตไปยัง API server หากจำเป็น
- จากนั้นจะแยกสาขาตามว่ามี annotation
AnnBindCompletedหรือไม่- หากไม่มี annotation จะไปที่
syncUnboundClaim - หากมี annotation จะไปที่
syncBoundClaim
- หากไม่มี annotation จะไปที่
- การประมวลผลจริงถูกแบ่งเป็นเมธอดสำหรับ unbound claim และ bound claim เพื่อให้อ่านง่าย
checkVolumeSatisfyClaim: ตรวจสอบข้อกำหนดของ PV
checkVolumeSatisfyClaimตรวจสอบว่า PV ที่ถูกร้องขอตอบสนองข้อกำหนดของ PVC หรือไม่- เงื่อนไขการตรวจสอบถูกระบุไว้ในโค้ดอย่างชัดเจน
- หาก PV มี
DeletionTimestampให้ถือว่า error - หากความจุของ PV น้อยกว่าความจุที่ PVC ขอ ให้ถือว่า error
- หาก
storageClassNameต่างกัน ให้ถือว่า error - หาก feature gate
VolumeAttributesClassเปิดอยู่ จะตรวจว่าค่าVolumeAttributesClassNameตรงกันหรือไม่ - หาก feature gate ปิดอยู่แต่ claim หรือ volume มี
VolumeAttributesClassNameให้ถือว่า error - หาก
volumeModeไม่เข้ากัน ให้ถือว่า error - หาก access mode ไม่เข้ากัน ให้ถือว่า error
- หาก PV มี
- หากผ่านทุกเงื่อนไข จะคืนค่า
nil
การจัดการ event ของ PVC แบบ delayed binding
emitEventForUnboundDelayBindingClaimสร้าง event เพื่อให้ข้อมูลแก่ claim ที่ยังไม่ถูก bind ในโหมด delayed binding- reason เริ่มต้นคือ
WaitForFirstConsumer - ข้อความเริ่มต้นระบุว่าจะรอการ bind จนกว่า consumer ตัวแรกจะถูกสร้างขึ้น
- หากมี Pod ที่อ้างอิง PVC นี้และยังไม่ได้ถูก schedule อยู่ reason จะเปลี่ยนเป็น
WaitForPodScheduled- หากมีหลาย Pod จะใส่ชื่อ Pod ทั้งหมดในข้อความ
- ใน volume scheduling จะพิจารณา Pod เพียงตัวเดียว แต่เพราะไม่รู้ว่าจะใช้ Pod ใด จึงใส่ทั้งหมด
syncUnboundClaim: การจัดการ PVC ที่ยังไม่ถูก bind
- หาก
claim.Spec.VolumeNameว่าง แปลว่าผู้ใช้ยังไม่ได้ร้องขอ PV เฉพาะ - ในกรณีนี้คอนโทรลเลอร์จะตรวจสอบโหมด delayed binding ของ claim และหา PV ที่เหมาะสมที่สุดด้วย
findBestMatchForClaim - หากไม่มี PV ที่เหมาะสม จะประมวลผลตามลำดับต่อไปนี้
- หากกำหนด StorageClass เริ่มต้นได้ ให้อัปเดต PVC แล้วจบการซิงโครไนซ์
- หากเป็น delayed binding และยังไม่อยู่ในสถานะ provisioning ให้สร้าง event สำหรับการรอ
- หาก claim มี StorageClass ให้พยายามทำ dynamic provisioning ด้วย
provisionClaim - มิฉะนั้น ให้บันทึก event
FailedBindingว่าไม่มี PV ที่ใช้ได้และไม่มี StorageClass
- หากมี PV ที่เหมาะสม จะเรียก
bindเพื่อ bind PV กับ PVC- เมื่อสำเร็จ จะบันทึก metric ของงาน provision + binding และล้าง timestamp cache
- หากเกิด error ระหว่างการบันทึก
syncClaimรอบถัดไปจะทำให้การ bind เสร็จสมบูรณ์
การจัดการ PVC ที่ร้องขอ PV เฉพาะ
- หาก
claim.Spec.VolumeNameไม่ว่าง แปลว่าผู้ใช้ร้องขอ PV เฉพาะ - หาก PV ที่ร้องขอไม่มีใน cache จะอัปเดตสถานะ PVC เป็น
Pendingแล้วลองใหม่ภายหลัง - หากมี PV ที่ร้องขอและ
volume.Spec.ClaimRefไม่มีค่า แปลว่า PV ยังไม่ได้ถูก claim- ตรวจสอบข้อกำหนดด้วย
checkVolumeSatisfyClaim - หากไม่ตรงข้อกำหนด ให้บันทึก event
VolumeMismatchและคง PVC ไว้ที่Pending - หากตรงข้อกำหนด ให้เรียก
bind
- ตรวจสอบข้อกำหนดด้วย
- หาก PV ที่ร้องขอถูก claim โดย PVC นี้อยู่แล้ว ให้เรียก
bindเพื่อทำให้การ bind เสร็จสมบูรณ์ - หาก PV ที่ร้องขอถูกผูกกับ claim อื่นแล้ว จะจัดการดังนี้
- หาก claim ไม่มี annotation ที่บอกว่าถูก bind โดย controller ให้บันทึก event
FailedBindingและคงไว้ที่Pending - หากดูเหมือนว่า controller เป็นผู้ bind แต่กลับถูกผูกกับ claim อื่น ให้คืน error ในสถานะ “should never happen”
- หาก claim ไม่มี annotation ที่บอกว่าถูก bind โดย controller ให้บันทึก event
syncBoundClaim: การจัดการ PVC ที่ถูก bind แล้ว
syncBoundClaimจัดการ PVC ที่มี annotationAnnBindCompleted- หากเป็น claim ที่ถูก bind แล้วแต่
claim.Spec.VolumeNameว่าง จะเปลี่ยนสถานะ claim เป็นClaimLost- ข้อความ event ระบุว่า bound claim สูญเสียการอ้างอิงถึง PV และข้อมูลใน volume สูญหายแล้ว
- หาก PV ที่ claim ชี้ไปไม่มีอยู่ ก็จะเปลี่ยนเป็น
ClaimLostเช่นกัน- ข้อความ event ระบุว่า bound claim สูญเสีย PersistentVolume และข้อมูลสูญหายแล้ว
- หากมี PV อยู่แต่
volume.Spec.ClaimRefไม่มีค่า จะถือว่า volume กลับเป็น unbound แล้วเรียกbindอีกครั้ง - หาก
ClaimRef.UIDของ PV เท่ากับ UID ของ claim จะถือว่าเป็นสถานะการ bind ปกติและเรียกbind- ในกรณีส่วนใหญ่ การเรียกนี้จะไม่ทำอะไร
- หาก PV ชี้ไปยัง claimant อื่น จะตั้ง claim phase เป็นสถานะ terminal
Lost
syncVolume: จุดเข้าของการซิงโครไนซ์ PV
syncVolumeเป็นเมธอดหลักที่ถูกเรียกเมื่อ volume ถูกสร้าง อัปเดต หรือซิงโครไนซ์ตามรอบเวลา- ไม่แยกประเภท event
- ขั้นแรกจะตั้ง migration annotation และ finalizer ที่ถูกต้องให้ PV และอัปเดตไปยัง API server หากจำเป็น
- หาก
volume.Spec.ClaimRefไม่มีค่า จะถือว่าเป็น volume ที่ไม่ได้ใช้งานและตั้ง phase เป็นAvailable - หากมี
ClaimRefแต่ UID ว่าง จะถือว่าเป็น PV ที่ถูกจองไว้สำหรับ PVC เฉพาะและตั้ง phase เป็นAvailable- PVC ดังกล่าวยังไม่ได้ bind กับ PV นี้ และ PVC sync จะเป็นผู้จัดการ
การจัดการ PV ที่หา claim ไม่พบ
- หาก PV ถูก bind กับ claim คอนโทรลเลอร์จะหา PVC ด้วย namespace/name ใน
ClaimRef - หากหา PVC ใน cache ไม่พบ จะตรวจสอบเพิ่มเติมในบางเงื่อนไข
- ตรวจสอบอีกครั้งจาก informer cache
- ตรวจสอบอีกครั้งจาก API server
- สำหรับ PV ที่สร้างโดย external PV provisioner หรือ external PV binder ภายใต้โหลดสูง PVC อาจยังไม่ได้ซิงโครไนซ์เข้ามาใน cache ภายในเครื่อง
- เพื่อไม่ให้ reclaim PVC ผิดพลาด จึงทำ การตรวจสอบซ้ำสองชั้น
- หากตัดสินว่าไม่มี claim จะเปลี่ยน volume phase เป็น
ReleasedและเรียกreclaimVolume- หาก phase เดิมเป็น
Failedจะไม่เขียนทับ - หาก reclaim policy เป็น
Retainจะบันทึก log ว่า PV อ้างถึง claim ที่ไม่มีอยู่
- หาก phase เดิมเป็น
กรณีที่การเชื่อมโยงระหว่าง PV และ PVC ไม่ตรงกัน
- หากมี claim อยู่แต่
claim.Spec.VolumeNameว่าง แปลว่า PVC ยังไม่มีชื่อ PV - หาก
volumeModeไม่ตรงกัน จะบันทึก eventVolumeMismatchทั้งฝั่ง PV และ PVC แล้วข้ามsyncClaim - หากไม่ใช่ mismatch จะเพิ่ม claim เข้า
claimQueueเพื่อให้syncClaimถูกเรียกในไม่ช้า- วิธีนี้ช่วยให้การ bind volume ที่ถูก provision แล้วเร็วขึ้น
- หาก
Spec.VolumeNameของ claim เท่ากับชื่อ volume ปัจจุบัน จะถือว่าเป็นการ bind ปกติและอัปเดต phase ของ volume เป็นBound - หาก claim ถูก bind กับ volume อื่นแล้ว จะจัดการตามสถานการณ์
- หากเป็น volume ที่ถูก provision แบบ dynamic และ reclaim policy เป็น
Deleteจะทำเครื่องหมายเป็นReleasedแล้วเรียกreclaimVolume - หากเป็น volume ที่ controller bind ให้ จะเก็บกวาดด้วย
unbindVolume - หากเป็น pointer ที่ผู้ใช้สร้างไว้ จะปล่อยไว้ตามเดิมแต่เรียก
unbindVolumeเพื่ออัปเดต phase และล้างClaimRef.UID
- หากเป็น volume ที่ถูก provision แบบ dynamic และ reclaim policy เป็น
การอัปเดตสถานะและการส่ง event
updateClaimStatusบันทึก status ของ PVC ไปยัง API server- การเปลี่ยน phase
- รีเซ็ต
AccessModes,Capacity,CurrentVolumeAttributesClassNameเมื่อไม่มี volume - อัปเดต access mode, capacity และชื่อ current volume attributes class เมื่อมี volume
- มีเงื่อนไขที่อัปเดต capacity เฉพาะตอนที่ claim กลายเป็น
Boundเท่านั้น- เพราะความแตกต่างระหว่างขนาด filesystem ของ PVC กับขนาด block device ของ PV อาจตั้งใจให้เป็นเช่นนั้น จึงไม่เขียนทับ capacity ของ claim ที่ bound อยู่แล้ว
- หาก feature gate
VolumeAttributesClassเปิดอยู่ จะตั้งCurrentVolumeAttributesClassNameระหว่างที่เปลี่ยนจาก pending เป็น bound- หลังจากนั้นควรให้ resizer หรือ admin override จัดการ และหากคอนโทรลเลอร์ตั้งค่าต่อเนื่องอาจเกิด race condition ได้
updateClaimStatusWithEventและupdateVolumePhaseWithEventจะส่ง event เฉพาะเมื่อ status/phase เปลี่ยนจริงเท่านั้น
การกำหนด StorageClass เริ่มต้น
assignDefaultStorageClassหา StorageClass เริ่มต้นและกำหนดให้ claim เมื่อ claim ไม่มี storage class- claim ที่มี storage class อยู่แล้วจะถูกข้าม
- หากไม่มี class เริ่มต้น จะไม่อัปเดตและคืนค่า
false - หากมี class เริ่มต้น จะตั้งชื่อ class ใน
claim.Spec.StorageClassNameแล้วอัปเดตไปยัง API server
ขอบเขตของไฟล์และข้อจำกัดที่ระบุชัด
- ตาม metadata ของไฟล์ที่แสดงบนหน้า GitHub
pv_controller.goมี 2038 บรรทัด, 1864 LOC, 91 KB - เนื้อหาที่ให้มาครอบคลุมตั้งแต่ส่วนต้นของไฟล์ไปจนถึงจุดเริ่มต้นของฟังก์ชัน
bindVolumeToClaimเท่านั้น ส่วนที่เหลือต่อไปยังลิงก์ raw view - ดังนั้นสรุปนี้จำกัดอยู่ที่โครงสร้างคอนโทรลเลอร์ คอมเมนต์ด้านการออกแบบ สาขาการซิงโครไนซ์หลัก และลอจิกอัปเดตสถานะที่ปรากฏในเนื้อหาโค้ดที่ให้มา
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
ผมไม่แน่ใจว่าแปลกไหมที่โค้ดในไฟล์นี้ให้ความรู้สึกเหมือน โค้ด Go ธรรมดาๆ จริงๆ เพราะเป็น Go เลยดูเยิ่นเย้อ และเพราะไม่ได้พึ่งพา abstraction ลึกๆ เลยดูยาวขึ้น แต่ตัวโค้ดเองก็ดูเป็นแบบทั่วไป
abstraction เป็นดาบสองคม วิธีแบบนี้ก็โอเค และถ้าไม่มีคำนำ ผมคงไม่คิดทบทวนเรื่องสไตล์การเขียนซ้ำด้วยซ้ำ อาจเป็นความแตกต่างที่เกิดจากการมีประสบการณ์กับซอฟต์แวร์องค์กร มากกว่าซอฟต์แวร์ระบบ สำหรับคนที่มีส่วนร่วมกับ Kubernetes อย่างสม่ำเสมอ คอมเมนต์เหล่านี้อาจดูไม่จำเป็น แต่ถ้าเป็นโค้ดที่ผู้อ่านในอนาคตไกลๆ ในสภาพแวดล้อมองค์กรต้องอ่านโดยไม่มีบริบท ที่ระดับความซับซ้อนนี้ผมอาจใส่คอมเมนต์มากกว่านี้ด้วยซ้ำ
เมื่อก่อนโค้ดแบบนี้รู้สึกเป็นเรื่องปกติ แต่ในช่วงประมาณ 10 ปี ที่ผ่านมา ดูเหมือนหลายคนให้ค่ากับความสั้นมากกว่าความชัดเจน
โดยเฉพาะในโค้ดสำคัญแบบนี้ ผมชอบความชัดเจนมากกว่าอย่างมาก ในอาชีพผมเคยเจอหลายครั้งที่โค้ดรวมเงื่อนไขหลายอย่างเข้าด้วยกันและละคอมเมนต์ที่อธิบายบริบททางธุรกิจกับความหมาย ทำให้ตัดสินไม่ได้ว่าพฤติกรรมปัจจุบันเป็นสิ่งที่ตั้งใจไว้หรือเป็นเรื่องบังเอิญ วิธีแบบนี้มักไม่ใช่โค้ดที่ทนต่อการเปลี่ยนแปลง แต่กลายเป็นโค้ดที่ขัดขวางการเปลี่ยนแปลง และอย่างน้อยก็ทำให้คนที่ไม่ใช่ผู้เขียนแก้ไขได้ยาก การสร้าง รั้วของเชสเตอร์ตัน ที่ไม่จำเป็นนั้นขัดกับความสามารถในการบำรุงรักษา
คอมเมนต์นี้น่าจะถูกเพิ่มเข้ามาหลังจากมีคนพยายามทำให้โค้ดง่ายขึ้นแล้วล้มเหลว เพื่อเตือนผู้ดูแลในอนาคตให้คิดอีกครั้งก่อนจะลองทำแบบเดียวกัน
คอมมิตที่เพิ่มคำเตือนคือ "Add note about space-shuttle code style"[1] และคอมมิตก่อนหน้านั้นทันทีคือ "Revert controller/volume: simplify sync logic in syncUnboundClaim"[2]
[1] https://github.com/kubernetes/kubernetes/commit/de4d193d45f6...
[2] https://github.com/kubernetes/kubernetes/commit/8a1baa4d64ca...
ผมก็เคยคิดคล้ายๆ กัน แต่พอเห็น
ifที่ซ้อนกันลึกๆ ก็เปลี่ยนความคิด ตรงส่วนนั้นผมน่าจะทำเป็นแขนงแบบ return ก่อน แน่นอนให้ความรู้สึกเหมือนทำเสร็จแค่ขั้นแรกของ "ทำให้ใช้ได้ ทำให้เร็ว ทำให้สวย" แล้วไม่ได้ทำขั้น "ทำให้สวย" เวลาแก้ปฏิสัมพันธ์ของสถานะที่ยุ่งยาก ผมก็เคยเขียนโค้ดหน้าตาไม่สวยและคอมเมนต์เยอะแบบนี้ แต่ปกติจะจัดระเบียบสักหน่อยก่อนรีวิว บางทีอาจดีกว่าถ้าติดแบนเนอร์ใหญ่ไว้บนสุดของไฟล์ว่า "อย่าพยายามทำให้โค้ดนี้ง่ายขึ้น" แต่ถึงอย่างนั้น มันก็ไม่ได้แย่มากแน่นอน
แม้มันอาจดูแปลก แต่คุณไม่ได้คิดอยู่คนเดียว ผมเองก็มองว่าโค้ดนี้ปกติอย่างสมบูรณ์ ผมเคยเขียนโค้ดและคอมเมนต์แบบนี้ในคอมโพเนนต์ที่รู้สึกว่าสำคัญต่อความน่าเชื่อถือของระบบ
ผมไม่เคยเห็นด้วยกับกระแส "โค้ดไร้คอมเมนต์" และเวลาย้อนกลับมาดูหลังผ่านไปหลายเดือนหรือหลายปี คอมเมนต์ที่ผมเขียนไว้ก็มักมีค่ามากสำหรับตัวผมในอนาคตบ่อยครั้ง นึกภาพไม่ออกเลยว่าจะไล่ประกอบตรรกะที่ฝังอยู่ในคอมโพเนนต์ที่ซับซ้อนระดับนี้ขึ้นมาใหม่โดยไม่มีคอมเมนต์ที่แข็งแรงได้อย่างไร
โดยเฉพาะคำอธิบายที่บอกว่า
ifทุกอันมีคอมเมนต์elseที่คู่กัน ดูเหมือนไม่ได้เป็นจริงอย่างมั่นคงนักifจำนวนมากที่ไม่มีคู่เป็นแค่การตรวจif (err != nil) {หรือการ return ก่อน แบบอื่นๆ แต่ถึงตัดพวกนั้นออกไป ก็ยังดูเหมือนมีifที่ไม่มีคู่กันอยู่อย่างไรก็ตาม จากประสบการณ์ซอฟต์แวร์องค์กร คอมเมนต์เพิ่มเติมก็ไม่ได้มีเยอะเสมอไป ใน codebase มีคอมเมนต์
// end ifระบาดเหมือนโรค แต่คอมเมนต์ที่อธิบายจริงๆ กลับพบได้น้อยบทความเกี่ยวกับคุณภาพซอฟต์แวร์ของ Space Shuttle: https://archive.is/HX7n4
ถ้าตัดตอนมา เหตุผลที่ซอฟต์แวร์นี้น่าทึ่งไม่ใช่ว่ามันทำงานได้มากแค่ไหน แต่อยู่ที่มันทำงานได้ดีเพียงใด กล่าวคือ ไม่เคยแครช ไม่ต้องรีบูต ไม่มีบั๊ก และใกล้เคียงความสมบูรณ์แบบในระดับที่มนุษย์ทำได้ เวอร์ชันสามรุ่นสุดท้ายมีรุ่นละ 420,000 บรรทัด แต่มีข้อผิดพลาดเพียงรุ่นละหนึ่งรายการ และทั้ง 11 เวอร์ชันสุดท้ายมีข้อผิดพลาดรวม 17 รายการ ว่ากันว่าโปรแกรมเชิงพาณิชย์ที่มีความซับซ้อนเท่ากันน่าจะมีข้อผิดพลาดประมาณ 5,000 รายการ
มันแพงและช้ามากจนการพิสูจน์ความถูกต้องของซอฟต์แวร์ด้วย proof assistant สมัยใหม่ น่าจะถูกกว่า เร็วกว่า และปลอดภัยกว่าจริงๆ มาก โครงการอย่าง seL4, CompCert แสดงให้เห็นว่าควรทำอย่างไร
ผมเข้าใจเจตนาของ
// KEEP THE SPACE SHUTTLE FLYING.แต่ก็น่าขำอยู่หน่อยที่ในคอมเมนต์อ้างถึงระบบที่ไม่ได้ใช้งานแล้วเพราะมีประวัติความปลอดภัยไม่ดีอีกสัก 10 ปี ผู้คนจะยังจดจำ Space Shuttle ในแง่ดีไหม?
ปัญหาความปลอดภัยของ Space Shuttle ส่วนใหญ่เป็น ปัญหาฮาร์ดแวร์ ไม่ใช่ปัญหาซอฟต์แวร์
ใน "Appendix F - Personal Observations on Reliability of Shuttle" [0] ซึ่งเป็นภาคผนวกของ Richard Feynman ในรายงานอุบัติเหตุ Challenger ปี 1986 กล่าวไว้ดังนี้
เขาเน้นเป็นพิเศษถึงคุณภาพของซอฟต์แวร์ avionics ในฐานะตัวอย่างว่าโครงการรัฐบาลขนาดใหญ่และซับซ้อนอย่าง Shuttle ก็สามารถทำวิศวกรรมได้อย่างถูกต้อง และไม่ได้ถูกกำหนดมาโดยตัวมันเองว่าจะต้องคุณภาพต่ำหรือเสี่ยงภัย
0: https://www.nasa.gov/history/rogersrep/v2appf.htm
เคยพาคนและอุปกรณ์ขึ้นสู่อวกาศและพากลับบ้านในภารกิจที่ประสบความสำเร็จมากกว่า 100 ครั้งอย่างมาก ทุกวันนี้ก็ยังมองในแง่ดี และมีแนวโน้มว่าจะเป็นเช่นนั้นต่อไป ในแง่ความก้าวหน้าของมนุษย์และผลสุทธิ ถือว่าเป็นความสำเร็จ
สิ่งที่ทำให้ Shuttle ยุติลงไม่ใช่ประวัติความปลอดภัยที่ย่ำแย่ แต่เป็น ต้นทุน และการคาดการณ์ว่าความปลอดภัยในอนาคตจะลดลง
แม้อุบัติเหตุ Shuttle สองครั้งจะทำให้นักบินอวกาศเสียชีวิตมากกว่าภัยพิบัติอื่น ๆ ของ NASA แต่เมื่อพิจารณาความยากของสิ่งที่เกิดขึ้นจริง ประวัติด้านความปลอดภัยก็น่าทึ่งมาก โค้ดดูดีมาก
สถานการณ์ของ Space Shuttle ซับซ้อนกว่าการบอกง่าย ๆ ว่าความปลอดภัยไม่ดี หากดูตามภารกิจ สถิติดีกว่ายานปล่อยอื่น ๆ ด้วยซ้ำ Shuttle มีภารกิจถึงขั้นเสียชีวิต 2 ครั้งจาก 135 ครั้ง ส่วน Soyuz สมัยสหภาพโซเวียตมี 2 ครั้งจาก 66 ครั้ง และ SpaceShipTwo มีสถิติแย่อย่างน่ากลัวคือภารกิจถึงขั้นเสียชีวิต 1 ครั้งจากการบินเพียง 12 ครั้ง
อย่างไรก็ตาม Space Shuttle มีความจุลูกเรือมากกว่าที่ภารกิจส่วนใหญ่ต้องการอยู่มาก ต่างจาก Apollo หรือ Soyuz ที่มี 3 คน มันบรรทุกได้สูงสุด 8 คน และเมื่อคิดว่าภารกิจส่วนใหญ่ของสหภาพโซเวียต/Roscosmos, ESA, CNSA เป็นภารกิจอัตโนมัติไร้คนขับทั้งหมด ก็ไม่มีลูกเรือให้ตกอยู่ในความเสี่ยงตั้งแต่แรก บางทีอุปมานี้อาจเหมาะกับ Kubernetes มากกว่า: เป็นระบบที่ถูกวิศวกรรมมาอย่างสูง ทรงพลัง ใช้งานได้หลากหลาย แต่ต้องใส่ใจมาก และอาจถูกใช้มากเกินความจำเป็นเล็กน้อย
หากใช้มาตรวัดที่พบบ่อยที่สุดคือจำนวนผู้โดยสาร-ไมล์ Space Shuttle จัดอยู่ในกลุ่มยานพาหนะที่ปลอดภัยที่สุดเท่าที่เคยสร้างและบินมา
ในฐานะคนที่วัยเด็กอยู่ในยุค 1980s พอดี ถามตรง ๆ ว่า จะไม่จดจำมันในแง่ดีได้อย่างไร หรือว่าเด็กเกินไปจนมองโปรแกรมนี้กับภารกิจและความสำเร็จทั้งหมดแบบย้อนหลังเท่านั้น และมีแต่มุมมองที่ถูกบรรยากาศยุคปัจจุบันซึ่งเน้นผู้รับเหมาด้านอวกาศเอกชนหล่อหลอมไว้?
เรื่องที่ Richard Hipp ปรับโค้ด SQLite ให้สอดคล้องกับมาตรฐานการบินก็น่าสนใจมากเช่นกัน: https://corecursive.com/066-sqlite-with-richard-hipp/#testin...
ส่วนนี้ทำให้นึกถึง การตรวจสอบความครบถ้วน ในโค้ด TypeScript พยายามใช้อยู่เสมอ
https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...
satisfies neverแบบใหม่กว่านั้นเหมาะกับงานนี้มาก และยังสะดวกแม้ในกรณีที่ชอบใช้เชนif elseคุณอาจชอบ
ts-patternก็ได้https://github.com/gvergnaud/ts-pattern
ถ้าดูเฉพาะกรณีที่ใส่
elseชัดเจนให้กับทุกifที่ไม่ได้เล็กน้อยโดยสิ้นเชิง ก็สงสัยว่าโค้ดนี้จะเรียบง่ายขึ้นแค่ไหน หากผู้เขียน Kubernetes ออกแบบโดยยึด structural pattern matching แทนบล็อกif/elseภาษากระแสหลักหลายภาษาที่รองรับ structural pattern matching มีเครื่องมือที่ตรวจสอบได้ตอนคอมไพล์ว่า matching ครบถ้วนหรือไม่ และเพียงเท่านั้นก็อาจเป็นวิธีที่เป็นธรรมชาติในภาษา พร้อมเพิ่มความหนาแน่นของข้อมูลในโค้ดได้
การอภิปรายเมื่อปี 2018: https://news.ycombinator.com/item?id=18772873
แค่กวาดตาดูโค้ดคร่าว ๆ แต่พูดตรง ๆ มันไม่ได้ดูแย่ขนาดนั้น มีบางส่วนที่ผมคงทำต่างออกไป แต่ผมเห็นโค้ดที่แย่กว่านี้มากมาเยอะแล้ว
อย่างน้อยโค้ดนี้ก็ทำตามกฎหนึ่งอย่าง ทุกอย่างดูผ่านการคิดมาแล้ว และให้ความรู้สึกว่ามีระเบียบวิธีบางอย่างอยู่ในความวุ่นวายนี้ ผมเลือกโค้ดแบบนี้ได้ทุกเมื่อ มากกว่าจับฉ่ายแบบที่เห็นมาหลายครั้ง ซึ่งปะปนสไตล์กัน เขียนแบบขี้เกียจ และมีโครงสร้างไม่เป็นเหตุเป็นผล
สงสัยว่าทำไมถึงสร้างแนวปฏิบัติด้าน "ความปลอดภัย" ขึ้นมาใหม่ แต่กลับมองข้าม best practices ด้านวิศวกรรมซอฟต์แวร์ ที่มีเอกสารรองรับ
โมดูลยาว 2,000 บรรทัด เมธอดยาว 200 บรรทัด และการซ้อน
if3–4 ชั้น มักถือว่าเป็นอันตราย คอมเมนต์ที่บอกแค่ว่าทำอะไร ไม่ใช่ทำไม ก็ไม่มีประโยชน์นัก และมีแนวโน้มคลาดเคลื่อนจากโค้ดจริง ยังเห็นการใช้nilที่ไม่จำเป็นด้วย แม้ยังไม่ลงลึกถึงปัญหาอย่าง coupling หรือหลัก single responsibility ก็เห็นประเด็นผิวเผินเหล่านี้แล้วถ้าคิดว่าสิ่งเหล่านี้เป็นอันตราย แนะนำให้อ่าน "John Carmack on Inlined Code"
http://number-none.com/blow/john_carmack_on_inlined_code.htm...
"โค้ดควบคุมการบินของจรวด Armadillo มีแค่ไม่กี่พันบรรทัด ผมจึงจับฟังก์ชัน tic หลัก แล้วเริ่ม inline subroutine ทั้งหมด ผมพูดไม่ได้ว่าพบบั๊กที่ซ่อนอยู่ซึ่งอาจทำให้เกิดการตกจริง ๆ แต่พบตัวแปรบางตัวที่ถูกตั้งค่าหลายครั้ง และ control flow บางจุดที่ดูน่าสงสัยเล็กน้อย โค้ดสุดท้ายเล็กลงและสะอาดขึ้น"
ถ้า Carmack เห็นคุณค่าในแนวทางนี้ ก็คงไม่ควรรีบปัดตก ความคิดเห็นต่อ ๆ มาก็น่าอ่านเช่นกัน
"ในช่วงหลายปีหลังจากเขียนบทความนี้ ผมมอง pure functional programming ในแง่บวกขึ้นมาก แม้ใน C/C++ หากใช้อย่างสมเหตุสมผล... ถ้ามันเริ่มรับมือยาก ให้หาวิธีแยกบล็อกออกเป็น pure function"
บางครั้งก็เป็นกรณี "ไม่มีทางอื่น(TM)"
การจำกัดจำนวนบรรทัดแบบตามอำเภอใจมักก่อให้เกิดการแตกเป็นชิ้นเล็กชิ้นน้อยโดยไม่จำเป็น พอรวม include, license, glue code และคอมเมนต์เข้าไปด้วย ก็กลายเป็น spaghetti ที่เข้าถึงยาก ลองพยายามรักษาเมธอดในโค้ดประสิทธิภาพสูงให้ไม่เกิน 200 บรรทัดดูสิ ประสิทธิภาพอาจร่วงลงเหมือนการบินของ Icarus ได้
เมื่ออ่านคอมเมนต์ในโค้ด จะเห็นว่าโค้ดนี้ถูกทำให้เรียบง่ายเป็นโมดูลเดียว และมีการใส่ know-how จำนวนมากเพื่อให้เข้าถึงได้ และที่สำคัญกว่านั้นคือดูแลต่อได้อย่างยั่งยืน สำหรับคนที่ไม่รู้ภาษา หรือไม่รู้ลอจิก คอมเมนต์ที่ช่วยให้เห็นภาพรวมว่าโค้ดทำอะไรนั้นมีประโยชน์มาก และอีก 6 เดือนให้หลัง แม้แต่โค้ดของตัวเองก็ยังดูแปลกหน้า ดังนั้นมันจึงมีประโยชน์กับตัวเองด้วย
เคยเขียนด้วยวิธีที่ “ปลอดภัย” แบบนี้มาค่อนข้างนาน แต่กลับสร้าง bug มากกว่าการจัดการข้อผิดพลาดแบบ railway-oriented ผ่าน early return มาก และยังใช้เวลาแก้นานกว่ามากด้วย
ถ้าใส่
elseอย่างชัดเจนให้ทุกบล็อกifความซับซ้อนจากการต้องจำบริบทปัจจุบันจะพุ่งสูงมาก ผมคิดว่าควรปรับกฎนี้เป็น “ทุกบล็อกเงื่อนไขifต้อง early return หรือมีบล็อกelseที่สอดคล้องกัน” จะสมเหตุสมผลกว่า แพตเทิร์นif (cond) { การจัดการพิเศษ }ทำให้เสี่ยงกว่าและให้เหตุผลตามได้ยากกว่า early return อย่างชัดเจนไม่มีสิ่งที่เรียกว่า ชุด best practice ที่เป็นมาตรฐานเพียงหนึ่งเดียว
ความยาวของฟังก์ชันหรือจำนวนบรรทัดโค้ดในไฟล์เองไม่ได้เป็นโทษหรือเป็นประโยชน์โดยเนื้อแท้ แต่ละภาษามีมุมมองของตัวเองว่าควรจัดโครงสร้างโค้ดอย่างไร แต่ไม่มีสิ่งใดอ้างได้ว่าเป็น “แนวปฏิบัติที่ดีที่สุด” Go ไม่ใช่ภาษาที่นิยมการแบ่งโค้ดออกเป็นไฟล์เล็ก ๆ จำนวนมาก
เมธอดยาว 200 บรรทัดไม่ได้ผิดโดยเนื้อแท้ หากโค้ดภายในเป็นเส้นตรงและรักษาระดับ abstraction เดียวกันไว้ได้ ก็อาจเป็นตัวเลือกที่ดีที่สุด
ทางเลือกอย่างการสร้างเมธอด 5 บรรทัดจำนวน 40 เมธอดอาจแย่กว่า เพราะถ้าจะเข้าใจภาพรวมต้องกระโดดไปมาหลายที่ และอาจทำลำดับการเรียกผิดได้ จำนวน permutation ที่เป็นไปได้มีถึง 40! แบบ
โค้ดแบบนี้ดูเหมือนเป็นตัวเลือกที่เหมาะมากสำหรับย้ายไปเป็น ระบบเชิงประกาศ อิงกฎ และขับเคลื่อนด้วยตาราง
วิธีนั้นเข้าใจและตรวจสอบได้ง่ายกว่าโค้ด imperative แบบเฉพาะกิจที่เต็มไปด้วย clause
ifโค้ดสกปรกประเภทนี้มักเป็นสัญญาณว่ามี abstraction ที่ขาดหายไป