- ระหว่างที่ EdgeDB ย้ายงาน network I/O จาก Python ไปยัง Rust การทดสอบ HTTP fetch ใหม่ที่อิง
reqwestดูเหมือนจะค้างเฉพาะบน ARM64 CI แต่ความจริงคือโปรเซสแครช - จากการวิเคราะห์ core dump พบว่าจุดที่มีปัญหาไม่ใช่โค้ด HTTP ใหม่ แต่เป็นภายใน
getenv()ของlibcซึ่งล้มเหลวขณะวนดูอาร์เรย์ตัวแปรสภาพแวดล้อมแล้วพยายามอ่านพอยน์เตอร์ที่ผิดคือ0x220 - อีกเธรดหนึ่งกำลังตั้งค่า
SSL_CERT_FILEและSSL_CERT_DIRผ่านเส้นทางของopenssl-probeทำให้setenv()จัดสรรenvironใหม่ และในเวลาเดียวกันเส้นทางจัดการข้อผิดพลาดของ Python ก็เรียกgetenv()จนเกิด race condition - แม้โค้ด Rust จะไม่มี
unsafeแบบชัดเจน แต่ระหว่างที่std::env::set_var()เปลี่ยน environment แบบ global ก็ไม่ได้ซิงก์กับ runtime อื่นหรือการเรียกlibcโดยตรงด้วยล็อกภายในของ Rust - วิธีแก้คือย้ายบน Linux จากแบ็กเอนด์
rust-native-tls/opensslของreqwestไปใช้rustlsแทน และทั้ง Rust 2024 edition กับ glibc ก็เริ่มเปลี่ยนไปในทิศทางที่ลดปัญหากลุ่มนี้
แครชที่โผล่เฉพาะบน ARM64 CI
- EdgeDB กำลังสร้างฟีเจอร์ HTTP fetch ใหม่ โดยย้ายโค้ด network I/O จำนวนมากจาก Python ไปยัง Rust
- ใช้
reqwestเป็นไลบรารี HTTP client และฟีเจอร์นี้ผ่านทั้งบนเครื่องโลคัลและ CI runner แบบ x86_64 แต่ล้มเหลวเป็นพักๆ บน ARM64 CI runner - ตอนแรกดูเหมือน test runner ค้างแบบไม่มีกำหนด และใน CI log ก็ไม่มี error มีเพียงสถานะว่ามีเทสต์หนึ่งยังรันอยู่ ก่อนจะ timeout หลังผ่านไปหลายชั่วโมง
- สมมติฐานแรกคือความต่างของ memory model ระหว่าง Intel กับ ARM64
- Intel มี memory model ที่ค่อนข้างเข้มงวด และมีลำดับรวมที่ทุกโปรเซสเซอร์เห็นตรงกันสำหรับการเขียนหน่วยความจำ
- ARM ใช้ memory model ที่มีลำดับอ่อนกว่า โดยการเขียนอาจถูกมองเห็นในคนละลำดับกันระหว่างเธรดต่างๆ
ตามรอย core dump ในสภาพแวดล้อม Docker CI
- เครื่อง CI ตอนกลางคืนรันอยู่บน Amazon AWS จึงสามารถล็อกอินเป็น root จริงนอกคอนเทนเนอร์เพื่อดู
dmesgและ system log ได้ - เมื่อตามหา PID ที่ดูเหมือนค้างทั้งในและนอกคอนเทนเนอร์ กลับไม่พบโปรเซสนั้น จึงทำให้รู้ว่า ไม่ใช่ deadlock แต่เป็นแครช
- เนื่องจาก Docker container ใช้ process namespace ดังนั้น core dump จึงถูกส่งไปยัง Docker host และพบ core dump ของโปรเซส
python3ในjournalctl - ตอนเปิด core ด้วย
gdbครั้งแรก backtrace ใช้งานแทบไม่ได้ เพราะไม่มีไฟล์.soจากในคอนเทนเนอร์ - หลังคัดลอก
/lib,/usrและส่วนอื่นๆ ออกมาจากคอนเทนเนอร์ แล้วตั้งค่าsolib-absolute-prefixของgdbก็ยืนยันได้ว่าจุดแครชคือgetenv()ในlibc.so.6
พอยน์เตอร์ตัวแปรสภาพแวดล้อมที่เสียหายซึ่ง getenv() อ่านเข้าไป
- backtrace ทั้งหมดเป็นเส้นทาง
getenv()→__dcigettext()→strerror_r()→strerror()→PyErr_SetFromErrnoWithFilenameObjects() - ไม่ใช่โค้ด HTTP ใหม่ที่แครชโดยตรง แต่เป็น Python ที่กำลังสร้าง exception จาก errno แล้ววิ่งผ่านเส้นทางที่เกี่ยวข้องกับ gettext ก่อนจะเรียก
getenv() - อิมพลีเมนเทชันของ
getenv()ใน GLIBC 2.17 จะวนผ่านenvironของ POSIX ซึ่งเป็นรายการchar **และตรวจพอยน์เตอร์สตริงของตัวแปรสภาพแวดล้อมไปจนถึงพอยน์เตอร์NULLตัวสุดท้าย - จาก disassembly และสถานะรีจิสเตอร์
getenv()แครชตอนพยายามอ่านไบต์จากแอดเดรสx19 = 0x2200x220เป็นแอดเดรสหน่วยความจำที่ใช้ไม่ได้อย่างชัดเจน- เมื่อตรวจ
environเอง รายการตัวแปรสภาพแวดล้อมปัจจุบันกลับดูสอดคล้องกันดี
สาเหตุ: race condition ระหว่าง setenv() กับ getenv()
setenv()ไม่ใช่ฟังก์ชันที่เรียกได้อย่างปลอดภัยในสภาพแวดล้อมหลายเธรด และมีการค้นพบซ้ำมาหลายครั้งแล้วว่ามันทำให้เกิดแครชประหลาดในgetenv()ของlibc- เมื่อนำ disassembly มาเทียบกับโค้ด C พบว่า
x20ตรงกับพอยน์เตอร์epที่ใช้เดินอาร์เรย์environ - ตอนเกิดแครช
x20มีค่า0x248b5000ขณะที่environปัจจุบันอยู่ที่0x28655750ซึ่งห่างออกไปราว 60MB - เมื่อเปรียบเทียบหน่วยความจำรอบอาร์เรย์ environment เก่ากับ
environปัจจุบัน พบว่าความต่างสุดท้ายอยู่ที่รายการSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtและSSL_CERT_DIR=/etc/ssl/certs - จึงมองได้ว่าอีกเธรดหนึ่งย้าย
environระหว่างการเรียกsetenv()และgetenv()ยังคงอ่านอาร์เรย์ environment เก่าต่อไป จนกลายเป็นสถานะ use-after-free
การเชื่อมโยงกับ openssl-probe และ TLS backend
- จาก issue เก่าที่เกี่ยวกับ
rust-native-tlsพบเบาะแสว่าopenssl-probeจะตั้งค่าตัวแปรสภาพแวดล้อมSSL_CERT_FILEและSSL_CERT_DIRเพื่อค้นหา system certificate - บน Linux หากใช้แบ็กเอนด์
opensslของrust-native-tlsก็จะเรียกเส้นทางนี้ - โค้ด
openssl-probeที่เป็นปัญหาตั้งค่าตัวแปรทั้งสองผ่านenv::set_var()โดยไม่มีunsafeแบบชัดเจนSSL_CERT_FILESSL_CERT_DIR
- การจับคู่แบบนี้ทำให้ โค้ด Rust ที่ไม่มี unsafe โต้ตอบกับการใช้งาน
libcภายในโปรเซสเดียวกันได้ไม่ดี และนำไปสู่การแครช
เหตุผลที่มันถูกทำซ้ำได้บน ARM64 Linux
- แครชนี้จะเกิดขึ้นเมื่อ
setenv()ย้ายอาร์เรย์ environment ด้วยreallocและในจังหวะเดียวกันอีกเธรดหนึ่งเรียกgetenv() - การทำให้เกิดซ้ำต้องมีหลายเงื่อนไขตรงกัน
- จำนวนตัวแปรสภาพแวดล้อมต้องพอดีจนทำให้เกิด
realloc - ต้องมี I/O failure ที่ไม่เกี่ยวกันถูกจับโดย
asyncio - เส้นทางจัดการข้อผิดพลาดของ Python ต้องเรียก
getenv()เพื่ออ่านตัวแปรสภาพแวดล้อมLANGUAGEในจังหวะเดียวกันพอดี
- จำนวนตัวแปรสภาพแวดล้อมต้องพอดีจนทำให้เกิด
- ค่าที่ผิดอย่าง
0x220มีขนาดใกล้กับขนาด environment เดิมเมื่อคิดเป็น 64-bit word0x220 / 8 = 68- ค่านี้ถูกเขียนทับลงในตำแหน่ง
NULLท้ายบล็อก environment เดิม และดูเหมือนเป็นค่าที่ system malloc ใช้บอกขนาดของ free block
- เพราะมีเงื่อนไขล่วงหน้าหลายอย่างมาก การที่มันทำซ้ำได้ค่อนข้างสม่ำเสมอบนแพลตฟอร์มเดียวจึงนับว่าโชคดีเสียด้วยซ้ำ
เบาะแสที่ยืนยันได้จาก disassembly บน ARM64
- ใน disassembly ของ
getenv()ดูสับสนอยู่พักหนึ่ง เพราะมองไม่เห็นชัดว่าx20เปลี่ยนค่าตรงไหน - กุญแจสำคัญคือ โหมดอ้างแอดเดรสแบบ pre-index ของ AArch64
ldr x19, [x20, #8]!ทำงานดังนี้x19 = *(x20 + 8)x20 = x20 + 8
- เพราะโหมดอ้างแอดเดรสนี้
x20จึงกำลังไล่เดินอาร์เรย์พอยน์เตอร์ของตัวแปรสภาพแวดล้อมอยู่ แม้จะไม่ได้ถูกเขียนไว้ทางซ้ายแบบชัดเจน
การแก้ไขที่นำมาใช้และความเปลี่ยนแปลงของโปรเจกต์ที่เกี่ยวข้อง
- สุดท้ายจึงตัดสินใจย้ายบน Linux จากแบ็กเอนด์
rust-native-tls/opensslของreqwestไปใช้rustlsแทน - เดิมทีเหตุผลที่เลือก native TLS backend ก็เพื่อหลีกเลี่ยงการต้องพก TLS engine สองตัวพร้อมกันระหว่างย้ายโค้ด Python ไปเป็น Rust
- แต่หลังจากปัญหานี้ จึงตัดสินใจว่าในระยะสั้นการพก TLS engine สองตัวก็ยอมรับได้
- อีกทางเลือกหนึ่งคือเรียก
try_init_ssl_cert_env_vars()ครั้งแรกขณะถือ GIL ของ Python อยู่- Rust มีล็อกภายในเพื่อกัน race ระหว่างโค้ด Rust ด้วยกันเองเวลาอ่านและเขียน environment
- แต่ไม่ได้กันกรณีที่โค้ดภาษาอื่นเรียก
libcโดยตรง - หากถือ GIL อยู่ อย่างน้อยก็จะกันการแข่งขันกับ Python thread ได้
- โปรเจกต์ Rust รับรู้ปัญหานี้อยู่แล้ว และมีแผนทำให้ฟังก์ชัน setter ของ environment กลายเป็น
unsafeใน 2024 edition - ฝั่งโปรเจกต์ glibc ก็เพิ่งเพิ่มการเปลี่ยนแปลงที่หลีกเลี่ยง
reallocและยอมให้ environment array เก่ารั่วทิ้งไป เพื่อเพิ่ม thread safety ของgetenv()
4 ความคิดเห็น
setenvไม่เป็น thread-safe และ C ก็ไม่ต้องการแก้ไขเรื่องนี้ฟังก์ชัน
setenvก่อปัญหาอีกแล้วผมคิดว่าจะตั้งชื่อว่า "ความไม่ปลอดภัยด้านเธรดของ C stdlib แม้แต่ Rust ที่ว่าปลอดภัยก็ยังช่วยไม่ได้" ครับ :)
เข้าใจอย่างชัดเจนแล้ว
ความคิดเห็นบน Hacker News
ประเด็นใหญ่ที่สุดตรงนี้คือ ใน edition ถัดไปของ Rust ฟังก์ชันตั้งค่าตัวแปรสภาพแวดล้อม จะกลายเป็น unsafe
ถ้าโชคดี ผลกระทบน่าจะลามลงไปถึง crate ต่าง ๆ ที่ทำให้เกิด crash แบบนี้ด้วย และระหว่างนี้ก็มี issue ถูกเปิดไว้ที่ upstream: https://github.com/alexcrichton/openssl-probe/issues/30
getenvกับsetenvหรือunsetenvจากคนละเธรดได้อย่างปลอดภัยนั้น ยังไม่ได้รับการแก้ไขจริงทางออกที่น่าเชื่อถือดูเหมือนจะมีเพียงการเปลี่ยนให้ฟังก์ชันเหล่านี้ต้องจับ mutex เสมอ
แต่น่าเสียดายที่ ecosystem ไม่ได้เป็นแบบนั้น: https://github.com/seanmonstar/reqwest/blob/master/Cargo.tom...
____UNSAFE_payattention__nevermindthatthisappears50timesinthisfile___ไปแล้วในเว็บเฟรมเวิร์กก็คล้ายกัน Vue มี directive
v-htmlส่วน React มีdangerouslySetInnerHTMLซึ่งในแง่นี้ผมมองว่า ฝั่ง Vue ดีกว่าอย่างชัดเจนset_varและremove_varใน standard library ของ Rust จะเริ่มต้องใช้บล็อกunsafe {}อย่างถูกต้องใน edition ถัดไปคือ 2024 editionเอกสารตอนนี้ก็กล่าวถึงประเด็นด้านความปลอดภัยแล้ว แต่การทำให้ฟังก์ชันเหล่านี้เป็น safe ตั้งแต่แรกเป็นความผิดพลาด และภาษาระดับสูงกว่านี้หลายภาษาก็ทำผิดแบบเดียวกัน
https://doc.rust-lang.org/stable/std/env/fn.set_var.html
glibc มีแพตช์ที่ทำให้
getenvปลอดภัยขึ้นในกรณีที่สภาพแวดล้อมถูกแก้ไขมากกว่าเดิม แต่ใน C ยังสามารถเข้าถึงenvironได้โดยตรงอยู่ ดังนั้นในสถานการณ์ที่มีการแก้ไข จึงไม่อาจปลอดภัยได้อย่างสมบูรณ์: https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6f...จะเกิดหน่วยความจำรั่วขนาดคงที่ในเชิง amortized ต่อหนึ่งตัวแปรสภาพแวดล้อมที่ยังใช้งานอยู่ แต่ตัวแปรเองก็มีการรั่วแบบนั้นอยู่แล้ว แถมยังขึ้นกับความยาว และรวมถึงค่าที่ไม่ได้ใช้อีกต่อไปด้วย
ดูเหมือนว่าน่าจะมี use case แบบพยาธิสภาพบางอย่าง ที่ทำให้หน่วยความจำเพิ่มขึ้นไม่สิ้นสุดแม้ในโปรแกรมที่ถูกต้องตาม API
เป็นเรื่องน่าสนใจแต่ก็รู้สึกคาใจ เพราะดูเหมือนเป็นการใส่บั๊ก หน่วยความจำเพิ่มขึ้นไม่สิ้นสุด ให้กับโปรแกรมที่ทำตาม API เพื่อแก้โปรแกรมที่ละเมิดกฎด้วยการใช้ API จากหลายเธรด ให้ความรู้สึกว่าเป็น pragmatism มากกว่า dogma
unsafeต่อให้ผู้ดูแล C standard library คัดค้านการทำให้
setenvปลอดภัยสำหรับหลายเธรด อย่างน้อยก็ควรกำหนด API ใหม่ที่ปลอดภัยต่อเธรด ไม่ว่าจะภายใน POSIX หรือทำเป็นมาตรฐานโดยพฤตินัยก่อนแล้วให้ POSIX รับไปทีหลังถ้าเอาเวลาที่ใช้ไปกับการอธิบายว่าทำไมทำอะไรไม่ได้ มาแก้ปัญหานี้ ก็น่าจะมีตัวแทนของ
setenvแบบเก่าที่สามารถเลิกใช้และถอดออกจากโปรเจกต์ซอฟต์แวร์จำนวนมากได้แล้วเมื่อเห็นว่า glibc กำลังทำการเปลี่ยนแปลงที่แทบจะกำจัดปัญหานี้ออกไปได้ คำพูดของผู้ดูแล Musl ที่ว่าภายใน Musl แก้ไม่ได้จึงฟังไม่ค่อยมีน้ำหนัก
extern char **environ;ตราบใดที่
environยังเข้าถึงได้แบบสาธารณะ ก็ไม่มีหลักประกันเลยว่าsetenvกับgetenvจะถูกใช้ด้วยซ้ำ เพราะสองอย่างนี้ไม่ใช่สิ่งบังคับถ้ากำจัด
environได้ การทำให้setenvและgetenvปลอดภัยต่อเธรดก็ค่อนข้างง่าย แต่ถ้ากำจัดไม่ได้ก็เป็นไปไม่ได้ ถึงอย่างนั้นก็ยังโต้แย้งได้ว่าการทำให้setenvและgetenvปลอดภัยต่อเธรดเป็นการปรับปรุง แม้จะไม่ใช่ทางออกที่สมบูรณ์ก็ตามexec()ทั้งหมดที่ไม่ได้รับสภาพแวดล้อมเป็นอาร์กิวเมนต์ หรือที่ค้นหาPATHเพื่อหาไฟล์ executable ก็ดูเหมือนจะต้องมี การล็อก ด้วยการเจอบั๊กเกี่ยวกับตัวแปรสภาพแวดล้อมบน Linux เหมือนเป็นพิธีผ่านด่านอย่างหนึ่ง และบน Unix อื่น ๆ กลับมักจะเป็นปัญหาน้อยกว่าอย่างแปลก ๆ
Linus กับเคอร์เนลแก้บั๊กของ POSIX แบบใช้งานจริง คือทำให้มันไม่ระเบิดขึ้นมาจริง ๆ แต่ glibc กลับตามหลังอยู่ ทั้งที่ผู้คนพยายามลดปัญหานี้แม้เพียงเล็กน้อยมาหลายสิบปีแล้ว ตรงนี้ก็น่าขำอยู่
จริงอยู่ว่ามีสารพัดเรื่องปวดหัวอย่าง
TZแต่ถ้าแค่มีgetenv_r()ให้ใช้ ซิงโครไนซ์กับsetenv()และเตือนตอนคอมไพล์/ลิงก์เมื่อใช้getenv()ปัญหาจำนวนมากก็น่าจะหายไปแล้วยิ่งไปกว่านั้น ยังอาจทำแบบ คัดลอกเมื่อเขียน (COW) โดยทำให้พอยน์เตอร์ของสภาพแวดล้อมเป็นแบบอ่านอย่างเดียวก็ได้
แต่กลับโยนปัญหาไปให้แต่ละแอปพลิเคชัน ซึ่งเป็นความผิดพลาดครั้งใหญ่ เพราะผู้เขียนแอปแทบไม่มีทางรู้ได้ว่า dependency ทำอะไรอยู่ สถานการณ์ที่ผมเคยเจอเมื่อนานมาแล้วก็เป็นแบบนั้น และตอนนั้นผู้ขายไลบรารีแบบปิดซอร์สก็บอกให้เลิกใช้ Linux ซึ่งเป็น Unix clone ของเล่นนั่นเสีย
ปัญหาไม่ใช่ implementation แต่เป็น ตัว API เอง
setenv(),unsetenv(),putenv()และโดยเฉพาะenvironนั้นโดยเนื้อแท้แล้วไม่ปลอดภัยในโปรแกรมหลายเธรดgetenv_r()ก็ไม่ได้ช่วยได้ทั้งหมด เพราะระหว่างที่เธรดหนึ่งกำลังคัดลอกค่าเก่าของตัวแปรสภาพแวดล้อมลงบัฟเฟอร์ที่ให้มา อีกเธรดหนึ่งก็อาจเรียกsetenv()ได้แน่นอนว่า
getenv_r()แก้กรณีที่หลังจากรับค่าจากgetenv()แล้วอีกเธรดเรียกsetenv()จนทำให้หน่วยความจำนั้นใช้ไม่ได้ แต่ก็ไม่มีทางกันการเรียกอื่น ๆ ที่ทำให้ API พังได้libc อาจลดปัญหาบางส่วนได้ด้วยการจับ mutex ภายใน
getenv()/setenv()/putenv()/unsetenv()แต่ libc ก็ยังไม่มีทางรับประกันได้ว่าค่าที่getenv()คืนมาจะยังใช้ได้ยาวนานพอให้โค้ดฝั่งผู้เรียกนำไปใช้และก็ไม่มีวิธีดี ๆ ที่จะทำให้การเข้าถึง
environโดยตรงปลอดภัย อาจทำให้environเป็น thread-local ได้ แต่แบบนั้นมุมมองต่อ environment ของแต่ละเธรดอาจคลาดเคลื่อนกันถาวร และผลจากการเรียกgetenv_r()กับการตรวจenvironโดยตรงก็อาจต่างกันการรักษา ความเข้ากันได้ย้อนหลัง ในเรื่องนี้ยากจริง ๆ และแค่เพิ่ม mutex เพื่อป้องกันฟังก์ชันเหล่านี้ ก็อาจเปลี่ยนความหมายของโปรแกรมเดิมจนพังได้
เคยมีบทความว่า
setenvแย่มาก: https://www.evanjones.ca/setenv-is-not-thread-safe.htmlมีการถกเถียงด้วย และตั้งแต่คอมเมนต์แรกก็พูดถึงว่ามันก่อปัญหาใน Rust: https://news.ycombinator.com/item?id=38342642
ปัญหาที่เหลือส่วนใหญ่ตรงนี้ดูเหมือนจะเป็นเรื่องของ สภาพแวดล้อมการพัฒนา เขาทดสอบโดยใช้ Docker บนเครื่องระยะไกลในศูนย์ข้อมูลของ Amazon และเครื่องนั้นรายงานโปรเซสแครชไม่ได้
แถมในคอนเทนเนอร์ก็ไม่มีข้อมูล debug symbol เพียงพอที่จะได้ backtrace ด้วย ถ้าได้ backtrace ที่ชัดเจนตั้งแต่ครั้งแรกที่ล้มเหลว ก็คงเห็นชัดทันที
ที่น่าสงสัยตั้งแต่แรกคือ ทำไมถึงใช้
setenvกันแน่เรื่องนี้ทำให้นึกถึงขบวนการ 12-factor app ที่อดีตเพื่อนร่วมงานบางคนเคยเชื่อกันมาก หนึ่งใน “factor” เหล่านั้นคือการตั้งค่าแอปพลิเคชันควรทำผ่าน environment variable
ผมคิดมาตลอดว่ามันค่อนข้างงี่เง่า เพราะวิธีตั้งค่าแบบนั้นเป็นโครงสร้างที่เหมือนเอาค่าสตริงมาใส่รวมกันในตะกร้าภายใต้ namespace แบบแบน
ความเสี่ยงของ
getenv()/setenv()/environก็เป็นเหตุผลหนักแน่นอีกอย่างว่าทำไมไม่ควรใช้ environment variable สำหรับการตั้งค่าแน่นอนว่าไม่ได้มีทางเลือกที่ยอดเยี่ยมและรองรับดีเสมอไป ผมชอบไฟล์ตั้งค่ามากกว่า และก็ใช้ template configuration ที่ใส่แค่ค่าของ development, staging, production ได้ด้วย ปกติถึงแม้จะมีข้อเสียและกับดัก ผมก็ใช้ YAML และถึงจะมีรูปแบบไฟล์ตั้งค่าที่ดีกว่านี้ได้ แต่ผมมองว่า YAML ดีกว่า environment variable มาก
ใน NT นั้น environment variable สามารถมีชนิดและทำเป็น template ได้ และยังมี registry ซึ่งเป็นฐานข้อมูลการตั้งค่าที่มี namespace ด้วย ถึงจะเยิ่นเย้อและแปลก ๆ ก็เถอะ
นอกจากนี้ MSVC ยังมีเวอร์ชันที่ thread-safe ของฟังก์ชัน standard library แทบทั้งหมด
มักได้ยินนักพัฒนา C/C++ หน้าใหม่บ่นว่า MSVC ขาดความเข้ากันได้กับ POSIX แต่ดูเหมือนไม่ค่อยได้คิดลึก ๆ ว่านั่นหมายถึงอะไรจริง ๆ มันเหมือนแค่หวังว่าจะ cross-compatible กับโปรแกรม C ที่เขียนไว้ในยุค 1990 มากกว่า
มันขัดขวางความสามารถในการอนุมานพฤติกรรมจาก signature ของฟังก์ชันเพียงอย่างเดียว และทำให้ฟังก์ชันจำนวนมากที่ควรเป็น pure function กลายเป็น impure
ถ้ามีฟีเจอร์ภาษาให้ทำเครื่องหมายแอปว่าในโปรเซสใด ๆ ก็ตาม environment variable ใช้งานไม่ได้ และอ่านได้เพียงครั้งเดียว—ไม่ใช่ทีละตัวแปร แต่ อ่านทั้งหมดเป็นชุดได้ครั้งเดียวเท่านั้น—ผมคงใช้ทุกที่
getenv()เองนั้นไม่มีปัญหาเลย ปัญหาคือsetenv()ตามทฤษฎีแล้ว environment ควรถูกตั้งค่าก่อนแอปลึกลับนั้นเริ่มทำงาน ดังนั้นจึงไม่ควรจำเป็นต้องใช้สิ่งนี้
แต่การเป็น namespace แบบแบน ค่าที่เป็นสตริง และพื้นที่ global อิสระที่ทุกคนใช้ร่วมกันโดยไม่รู้ด้วยซ้ำว่าจะมีไลบรารีและโมดูลใดเข้ามา ก็ไม่ใช่ความคิดที่ดีอยู่แล้ว แม้จะไม่มีปัญหาความปลอดภัยของ
setenv()ก็ตามปัญหาส่วนใหญ่ที่คนพูดถึงกันตรงนี้ดูเหมือนมาจากการพยายามใช้ environment ในทางที่ผิด ให้เป็น key-value store สำหรับ mutable global state ไม่รู้ว่าทำไมถึงอยากทำแบบนั้น
JVM ถือว่า environment แทบจะเป็น immutable และบริษัทที่ใช้ Scala/Java อย่าง SoundCloud ก็อาจมีอิทธิพลต่อขบวนการ 12-factor app ด้วย ผมไม่เคยเจอกรณีที่ environment เปลี่ยนหรือก่อปัญหา threading เลย
แม้ environment จะเปลี่ยนไป สำเนา immutable ที่สร้างขึ้นตอน JVM เริ่มทำงานก็ยังคงเดิม และโค้ดที่โต้ตอบกับ environment ผ่าน Java API ปกติจะมองไม่เห็นการแก้ไขนั้น
ปัญหาของไฟล์ตั้งค่าคือการ parse เป็นเรื่องของแต่ละโปรเซส นั่นจึงทำให้ Linux/Unix เละเทะขนาดนั้น เครื่องมือแต่ละตัวมีธรรมเนียมและกลไกการตั้งค่าต่างกัน และไม่มีมาตรฐาน
ใน ecosystem ของ Docker นั้น ภายใน container จะทำอะไรก็ช่าง แต่อินเทอร์เฟซกับภายนอกคือการ mount volume แล้วทำตามวิธีตั้งค่าซับซ้อนของแต่ละแอป หรือไม่ก็ใช้ environment variable ไปเลย
ซอฟต์แวร์สมัยใหม่ส่วนใหญ่ที่รันด้วย Docker ทุกวันนี้เป็นมิตรกับ Docker จนควบคุมพฤติกรรมทั้งหมดผ่าน environment ได้ และในหลายกรณีก็เพียงพอ
ถ้าใช้ Docker Compose หรือ Kubernetes ก็จะได้รายการ environment variable ที่กำหนดวิธีเริ่มโปรเซสอยู่ในไฟล์ YAML ดังนั้นก็ได้โครงสร้างที่ต้องการในระดับหนึ่ง ผมไม่ได้ชอบ YAML แต่ก็ใช้งานได้ดีพอ และแม้ปัญหา syntax จะมีโอกาสทำให้ทั้งวันพังได้มาก ทางเลือกอื่น ๆ ก็ไม่ได้ไร้ปัญหาเหมือนกัน
ผมเองก็ใช้ สไตล์ 12-factor app แต่หลังจากข้อมูลเข้ามาในแอปแล้ว จะตรวจสอบและจัดเก็บ environment variable กับข้อมูลไว้ จากนั้นก็ไม่มีปัญหาอะไร
เป็นบทความที่ยอดเยี่ยมในการขุดคุ้ยบั๊กที่สังเกตเห็นได้ยาก
มีครบทั้งบั๊กที่เกิดเป็นครั้งคราว ความเฉพาะของสถาปัตยกรรม การซ่อนอยู่ใน dependency, Rust, Python GIL ไปจนถึง gettext
รายงานการแก้ปัญหาอย่างละเอียดแบบนี้เป็นแหล่งข้อมูลที่ใกล้เคียงกับการเจอเองที่สุด ในสถานการณ์ที่ dependency ของเรากำลังใช้สิ่งนั้นอยู่ แล้วเราจะไปรู้ได้อย่างไร จึงพูดง่าย ๆ ว่า “ก็อย่าใช้ X สิ” ได้ยาก
บอกว่า “เครื่อง CI ตอนกลางคืนรันบน Amazon AWS และมีข้อดีคือใช้ผู้ใช้ root จริง ๆ ไม่ใช่ container” แต่ขณะเดียวกันก็บอกว่า “นอก container ไม่มีไฟล์ที่จำเป็น และ container ก็ถูกทำให้ minimal มากจนติดตั้ง
gdbได้ไม่ง่าย”ตอนนี้ผู้คนสูญเสียความสามารถในการ build และ debug ในเครื่อง local โดยไม่พึ่ง cloud กับ container ไปแล้วหรือ?
แค่ทำเรื่องเล็กน้อยมาก ๆ ก็ต้องใช้ความซับซ้อนของ cloud และชั้น deployment สารพัด กลายเป็นการย้อนการปฏิวัติ PC กลับไป 100% สู่ยุค mainframe computing ที่เทอะทะและแพง
เหตุผลคือมีเงินอยู่ใน cloud และเพราะ cloud คือ DRM ถ้าเอาซอฟต์แวร์ไปไว้บนนั้นก็เก็บค่าสมัครสมาชิกได้ หลีกเลี่ยงไม่ได้ และรักษาการผูกมัดแบบสมบูรณ์ได้ตลอดไป หลายครั้งผู้ใช้เอาข้อมูลของตัวเองออกมาไม่ได้ด้วยซ้ำ
การทำ real-time analytics เพื่อปรับแต่งผลิตภัณฑ์ก็ทำได้สะดวก
สถาปัตยกรรมคอมพิวเตอร์เป็นสิ่งที่ไหลตาม business model มาอีกที mainframe ตายไปครั้งแรกเพราะไม่มีอินเทอร์เน็ตและ PC ถูกกว่า แต่ก็เพราะ vendor สูญเสียอำนาจในการผูกมัดไปมากด้วย
ตอนนี้มีวิธีชุบชีวิตโมเดลที่ทำกำไรได้สูงกว่ามากแล้ว เสรีภาพอันน่ารำคาญของผู้ใช้หายไป และพูดตามตรง เมื่อผู้ใช้ได้เสรีภาพแบบนั้น ก็มักไม่ยอมจ่ายเงิน ทำให้ธุรกิจซอฟต์แวร์คุณภาพดีเกิดขึ้นได้ยาก
เป็นไปได้มากว่า local ไม่สามารถ reproduce crash ได้ เครื่องของนักพัฒนาส่วนใหญ่น่าจะเป็น x86 และที่นั่นก็คงไม่ crash
การจัดการ crash ควรทำได้ดีกว่านี้ แต่พวกเขาก็ดูเหมือนจะรับรู้ปัญหานั้นอยู่แล้ว และไม่ใช่ประเด็นหลักที่พูดถึงตรงนี้
สถานะโกลบอลที่เปลี่ยนแปลงได้คือสิ่งชั่วร้าย ถ้าเป็นเพื่อนกัน ก็ไม่ควรปล่อยให้เพื่อนใช้สถานะโกลบอลที่เปลี่ยนแปลงได้
ไม่ชอบตัวแปรสภาพแวดล้อมเลย นี่คือ “วิถีแบบ Linux” แต่ควรหลีกเลี่ยงเหมือนโรคระบาด แนะนำอย่างยิ่ง
libc แย่มาก และโลกควรก้าวข้ามมันไปได้แล้ว
ปัญหาไม่ใช่ Linux ไม่ใช่สถานะหรือทรัพยากรโกลบอลที่เปลี่ยนแปลงได้ และไม่ใช่ libc
ปัญหาคือที่ทำงานไม่ให้เวลาพอที่จะทำงานให้ถูกต้องต่างหาก เช่น ถ้าจะจับปัญหาด้วย GDB ก่อนที่มันจะระเบิด หัวหน้าต้องให้เวลาพอในการดีบักและไล่ย้อนอย่างอดทน ทั้งตัวโค้ดและทุกอย่างที่โค้ดนั้นไปแตะ
มีเงินจำนวนมากเกินไปไหลไปอยู่กับโค้ดที่ยังสุก ๆ ดิบ ๆ น่าเศร้าแต่จริง
เจอปัญหาแบบนี้เยอะมาก จนสุดท้ายต้องแพตช์
getenv/setenv/putenvด้วยLD_PRELOAD