วันก่อนผมได้เล่าให้เพื่อนๆฟังไปแล้ว เกี่ยวกับปัญหาการใช้งาน Subversion บน Eclipse ซึ่งเมื่อผมทำการ Checkout โปรเจ็คออกมา บางโปรเจ็คก็ไม่มีปัญหาอะไร แต่บางโปรเจ็คก็เกิดอาการดาวโหลดบางไฟล์ไม่ได้ โดยฟ้อง
RA layer request failed
GET ... Internal Server Error
เกี่ยวอะไรกับ MySQL?
ทีแรกนึกว่ามีบางไฟล์เสียหาย แต่เมื่อผมทดลองซ้ำๆหลายๆรอบ ดูเหมือนว่าอาการจะไม่แน่นอน ไม่ซ้ำไฟล์เดิม และบางครั้ง Error ก็เปลี่ยนไปเป็น server close connection ผมจึงคิดว่าปัญหามันน่าจะเกิดที่ Apache Web Server มากกว่า โดยเมื่อเข้าไปดู Log ไฟล์ ก็แทบจะไม่มีอะไรบอกเกี่ยวกับ svn เลย แต่กลับมีข้อความเกี่ยวกับ MySQL เช่น query ไม่ได้ database connection lost อะไรทำนองนั้น ทีแรกผมก็ไม่ได้เอะใจ เพราะคิดว่าไม่เกี่ยวข้องกับ Subversion สักหน่อย แต่ทำไมมันดูเหมือนจะเกี่ยวกัน เพราะฟ้อง warning ตรงตอน GET ไฟล์ แต่เอาเถอะโปรแกรมรุ่นใหม่อาจจะมีการเปลี่ยนไปใช้ฐานข้อมูล MySQL ร่วมด้วยก็ได้ ผมก็เลยพุ่งเป้าไปที่ MySQL
ทีแรกก็ลองปรับค่า max_allowed_packet จาก 16M เป็น 64M ค่า max_connection จาก 100 เป็น 200 ค่า query_cache_limit จาก 1M เป็น 16M ด้วยเหตุผลที่ว่า อาการคล้ายๆกับ MySQL ทำงานไม่ได้ อาจจะเพราะตอน Checkout ไฟล์จำนวนมากๆ เกิด connection จำนวนมาก (ถ้าเป็น 1 ไฟล์ต่อ 1 connection) ก็จะเกิดปัญหาประมาณนี้ ซึ่งก็ช่วยให้สามารถ Checkout ไฟล์ได้มากขึ้น แต่สุดท้ายก็ตายทุกครั้ง
ผมเข้าไปดูค่าใน /var/log/mysql/error.log แล้ว ก็ไม่พบความผิดปกติอะไรเลย ดูเหมือน MySQL จะทำงานได้เป็นปกติดีทุกประการ ผมก็คิดไม่ออกว่ามันเกิดอะไรขึ้น และคิดว่า มันต้องมีปัญหาที่คำสั่ง SQL เพราะ error ใน Apache Web Server ระบุว่า query ไม่ได้ บางครั้งมีคำว่า "SELECSELECT ..." เหมือนคำสั่งมันกลายเป็นขยะไป ทำให้ระบบรวนไปหมด ผมจึงต้องลงลึกเข้าไปในระดับทุกๆคำสั่งที่ส่งให้กับ MySQL เลยว่ามีปัญหาอะไร ผมจึงไปตั้งค่า general_log_file และ general_log (ปกติจะปิดไว้) เพื่อดูค่าการทำงานในแต่ละคำสั่งของ MySQL ซึ่งจะบันทึกไว้ที่ /var/log/mysql/mysql.log (ปกติจะไม่มีไฟล์นี้อยู่)
สุดท้ายผมก็เห็นคำสั่ง
SELECT xxx FROM profiles WHERE login_name=? AND disabledtext = ''
ก็ได้ถึงบางอ้อว่า จริงๆแล้ว คำสั่งนี้เป็นคำสั่งที่เกิดจาก Apache Module ที่มีชื่อว่า mod_auth_mysql เป็นโมดูลที่เกี่ยวกับ การ Authentication เข้าใช้งาน และ Authorization สำหรับยืนยันสิทธิ์ในการเข้าถึงไฟล์ต่างๆของ Subversion ซึ่งผมเลือกใช้โมดูลนี้เพื่อตั้งค่าให้มีการใช้ account จากฐานข้อมูล MySQL ของ Bugzilla ดังนั้น ทุกๆครั้งที่มีการดาวโหลดไฟล์ จะมีการตรวจสอบสิทธิ์ทุกครั้ง และทำให้เกิดการ query ด้วยคำสั่งข้างบนซ้ำๆจำนวนมาก อาจจะทำให้เกิดอาการไม่เสถียร
Multi-Processing Module (MPM) Prefork vs. Worker
เมื่อรู้ว่าปัญหาเกิดจากโมดูล mod_auth_mysql ทำงานไม่เสถียรแล้ว ผมจึงคิดว่าน่าจะต้องปรับค่าที่เกี่ยวกับการจัดการประมวลผลโมดูล หรือที่เรียกว่า MPM ซึ่งเมื่อตรวจสอบ Apache Web Server ก็พบว่า มีการตั้งค่าแตกต่างไปจากรุ่นก่อน (Ubuntu 12.04) คือ การแยก MPM ออกเป็นโมดูล และเลือกตั้งค่าไว้เป็น mpm_event ในขณะที่ค่าเก่าที่ผมเคยตั้งไว้เป็น mpm_prefork ผมเองก็จำไม่ได้แล้วว่าทำไมถึงใช้ prefork แต่ไหนๆก็ไหนๆแล้ว ผมก็เลยทดลองปรับค่าดู ซึ่งค่าเดิมที่ตั้งไว้คือ default ตามข้างล่างนี้
<IfModule mpm_event_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxClients 150
MaxRequestsPerChild 0
</IfModule>
เมื่อทดลองปรับค่า เช่น MaxClient ลดให้น้อยลง MaxRequestsPerChild กำหนดให้เป็นค่า 1000 แทนที่จะเป็น 0 ซึ่งหมายถึงไม่จำกัด ค่า MaxSpareThreads ลดลง ค่า ThreadLimit ลดลง ThreadsPerChild ลดลง ผลปรากฏว่า เมื่อ Checkout การทำงานค่อนข้างดีขึ้น แต่สุดท้ายแล้วก็นิ่งไปนาน แล้วเกิด connection timeout ขี้น เมื่อดูใน Log ไฟล์ ไม่มีข้อมูลใดๆ เพราะ Apache Web Server ตายไปเรียบร้อยแล้ว ผมก็พยายามทดลองปรับค่าต่างๆแล้ว สุดท้ายสรุปว่าโมดูล mod_auth_mysql ไม่ใช่ thread safe โมดูล (ในขณะที่ข้อมูลจากบางเว็บไซต์ระบุว่าเป็น thread safe) ไม่สามารถใช้งานกับ mpm_worker หรือ mpm_event ได้
prefork เป็นการประมวลผลแบบไม่ใช้ thread โดย root process จะมีการแตก child process ออกมา และแต่ละ child process จะดูแล connection หลาย connection ต่อ process
worker เป็นการประมวลผลแบบใช้ thread โดย root process จะมีการแตก child process ออกมา และแต่ละ child process จะแตก thread ออกมาเพื่อดูแลแต่ละ connection
prefork เหมาะกับการทำงานที่รองรับโมดูลที่ไม่ได้ออกแบบมาให้เป็น thread safe และสำหรับประสิทธิภาพแล้ว ทั้ง prefork และ worker ทำงานได้พอๆกัน แต่ prefork จะใช้ทรัพยากรณ์ หน่วยความจำมากกว่า worker ส่วน event นั้นเป็น worker อีกรูปแบบหนึ่งที่จะจัดการ thread ได้ดีขึ้น
สรุปว่า mod_auth_mysql ไม่เป็น thread safe ต้องใช้กับ prefork เท่านั้น ซึ่งเมื่อผมทดลองปรับไปใช้ prefork ปัญหาทุกอย่างก็หมดไป นิ่งเสถียรดีมาก แต่ด้วยความคิดที่ว่า event มันดูดีกว่า prefork นะ ใช้ทรัพยากรณ์น้อยกว่า ผมจึงคิดว่า เราน่าจะหาทางใช้ event นะ
Apache DBD Module
ผมค้นไปค้นมาก็พบว่า Apache มีโมดูลที่ทำงานกับฐานข้อมูล SQL อยู่แล้ว ชื่อว่า mod_dbd ดังรายละเอียดในหน้า Apache DBD API ที่น่าสนใจคือ
- ไม่ยึดติดกับฐานข้อมูลใดๆ โดยจะต้องติดตั้ง driver สำหรับแต่ละฐานข้อมูลเพิ่ม เช่นฐานข้อมูลที่มี driver แล้วคือ MSSQL, SyBase, MySQL, Oracle, PostgreSQL, SQLite, และ ODBC
- ใช้กับ MPM ได้หลายแบบทั้งแบบ threaded หรือ non-threaded
- จัดการ connection pooling แบบไดนามิกส์เพื่อรองรับโปรแกรมใหญ่ๆได้
และนอกจากนี้ยังมีโมดูล mod_authn_dbd สำหรับการทำ Authentication และโมดูล mod_authz_dbd สำหรับการทำ Authorization ด้วย ทำให้ตอบโจทย์ที่ผมคิดไว้พอดี ผมจึงตัดสินใจทดสอบการใช้งานดู โดยติดตั้งโมดูลต่างๆ และแก้ไข config ไฟล์ ตามตัวอย่างใน document ของ mod_authn_dbd ดังนี้
# mod_dbd configuration
# UPDATED to include authentication cacheing
DBDriver pgsql
DBDParams "dbname=apacheauth user=apache password=xxxxxx"
DBDMin 4
DBDKeep 8
DBDMax 20
DBDExptime 300
<Directory /usr/www/myhost/private>
# mod_authn_core and mod_auth_basic configuration
# for mod_authn_dbd
AuthType Basic
AuthName "My Server"
# To cache credentials, put socache ahead of dbd here
AuthBasicProvider socache dbd
# Also required for caching: tell the cache to cache dbd lookups!
AuthnCacheProvideFor dbd
AuthnCacheContext my-server
# mod_authz_core configuration
Require valid-user
# mod_authn_dbd SQL query to authenticate a user
AuthDBDUserPWQuery "SELECT password FROM authn WHERE user = %s"
</Directory>
ในตัวอย่างนี้ จะมีการใช้ AuthBasicProvider โดยเน้นว่าให้ใช้ socache เพื่อทำ cache ให้กับ credential ด้วย ผมก็ได้ทำตามคือ ติดตั้ง socache เพิ่ม และที่สำคัญคือ AuthDBUserPWQuery ที่ต้องสั่ง query รหัสผ่านสำหรับผู้ใช้มาเพื่อตรวจสอบ โดยใส่ %s ไว้ตรงที่เป็นชื่อผู้ใช้ที่จะกรอกผ่าน browser มา ผลการรันปรากฏว่าเกิด Segmentation fault error ครับ Apache สตาร์ทไม่ขึ้น ผมจึงค่อยๆถอดบางคำสั่งที่ไม่จำเป็นออก และสุดท้ายพบว่าปัญหาอยู่ที่ AuthnCacheProvideFor และ AuthnCacheContext ครับ เมื่อตัดสองคำสั่งนี้ออก Apache ก็ทำงานได้ตามปกติ
Password Format
จากคำอธิบายของคำสั่ง AuthDBUserPWQuery ระบุว่า ผลของการ query จะนำเอาเฉพาะข้อความที่อยู่ใน row แรก column แรกเท่านั้นมาพิจารณาเป็นรหัสผ่านในรูปแบบรหัสที่เข้ารหัสแล้ว ซึ่งวิธีพิจารณามีสองแบบคือ แบบ Basic หรือ แบบ Digest ตามที่ได้ระบุ provider ไว้ (ผมขอไม่พูดถึงแบบ Digest นะครับ) เช่นในกรณีนี้ ผมใช้ AuthBasicProvider มีรายละเอียดรูปแบบของข้อมูลรหัสที่จะนำมาเปรียบเทียบตามที่ระบุไว้ชัดเจนในเอกสารคู่มือของ Apache เรื่อง Password Format โดยแบบ Basic มีอยู่ 5 รูปแบบคือ
- bcrypt: ข้อความ "$2y$" + ข้อความที่เข้ารหัสด้วยอัลกอริทึม crypt_blowfish
- MD5: ข้อความ "$apr1$" + ข้อความที่เข้ารหัสด้วยอัลกอริทึม MD5 ในแบบของ Apache เอง (ไม่เหมือนกับ MD5 อื่นๆ ผมเสียเวลาเข้าใจผิดทดสอบอยู่นานเหมือนกัน)
- SHA1: ข้อความ "{SHA}" + ข้อความที่เข้ารหัสด้วย Base64 ของอัลกอริทึม SHA-1 (ไม่ปลอดภัย)
- CRYPT: ข้อความที่เข้ารหัสด้วยฟังก์ชัน crypt บนระบบ Unix เท่านั้น (ไม่ปลอดภัย)
- PLAIN: ข้อความที่ไม่เข้ารหัส เฉพาะบนระบบ Windows และ Netware (ไม่ปลอดภัย)
อ๊ะๆ สังเกตเห็นอะไรไหมครับ รหัสผ่านที่เข้ารหัสแล้ว จะมีการใส่สัญลักษณ์พิเศษไว้เพื่อให้รู้ว่าเข้ารหัสด้วยวิธีไหน แต่ไม่ต้องกังวลนะครับ อัลกอริทึมที่เข้ารหัสพวกนี้ เป็นอัลกอริทึมที่ออกแบบมาให้ย้อนกลับได้ยาก ทางเดียวที่ง่ายกว่าคือ ต้องหารหัสมาเข้ารหัสด้วยวิธีเดียวกันแล้วได้ค่าที่ตรงกันจึงจะบอกได้ว่าเป็นรหัสที่ถูกต้อง ใครสนใจเรื่องรูปแบบของรหัสสามารถอ่านเพิ่มเติมจากลิ้งค์ที่ให้ไว้ได้นะครับ ในนั้นมีการแนะนำการใช้คำสั่ง htpasswd และคำสั่ง openssl เพื่อทดสอบดูว่า รหัสที่เข้าด้วยอัลกอริทึมแบบต่างๆแล้วได้รูปแบบเป็นอย่างไร
กลับมาเข้าเรื่องปัญหาของผมต่อนะครับ จากฐานข้อมูลผู้ใช้ใน MySQL ของ Bugzilla รหัสผ่านที่เข้ารหัสไว้แล้วนั้น มีรูปแบบไม่เหมือนกับที่ Apache ระบุไว้ ดังนั้น คำสั่งที่จะ query รหัสผ่านมานั้น จะต้องมีการจัดรูปแบบใหม่ให้ถูกต้อง โดยรูปแบบของ Bugzilla จะอยู่ในแบบ
รหัสที่เข้ารหัสแล้ว + "{" + ชื่อ Digest อัลกอริทึม + "}"
เช่น (ขอสมมติรหัสนะครับ)
123456789ABCDEFGHIjklmnopqrstuvwxyZ{MD5}
ถ้าหากผม query มาตรงๆ ก็จะทำให้ mod_authn_dbd ตรวจสอบรหัสผ่านไม่ถูก เพราะถ้าเป็น MD5 จะต้องมีรหัสเป็นประมาณ
$apr1$123456789ABCDEFGHIjklmnopqrstuvwxyZ
ซึ่งก็ไม่ใช่เรื่องยากสักเท่าไรนักที่จะใช้คำสั่ง substring ของ SQL ตัดและต่อคำเข้าไป แต่ปัญหาใหญ่ของผมตอนนี้คือ รหัส MD5 แบบทั่วไปที่ใช้อยู่ใน Bugzilla ซึ่งเขียนด้วยภาษา perl โดยใช้ Digest::MD5 นั้นไม่เหมือนกับ Apache MD5 หรือที่เรียกว่า APR1 ทำให้ผมต้องไปควานหาว่า perl มีอัลกอริทึมเข้ารหัสแบบไหนให้ใช้บ้าง ซึ่งถ้าเป็นไปตาม man page ของ Digest ก็คือมีดังนี้ (เรียงตามลำดับความเร็ว)
Algorithm Size Implementation MB/s
MD4 128 Digest::MD4 v1.3 165.0
MD5 128 Digest::MD5 v2.33 98.8
SHA-256 256 Digest::SHA2 v1.1.0 66.7
SHA-1 160 Digest::SHA v4.3.1 58.9
SHA-1 160 Digest::SHA1 v2.10 48.8
SHA-256 256 Digest::SHA v4.3.1 41.3
Haval-256 256 Digest::Haval256 v1.0.4 39.8
SHA-384 384 Digest::SHA2 v1.1.0 19.6
SHA-512 512 Digest::SHA2 v1.1.0 19.3
SHA-384 384 Digest::SHA v4.3.1 19.2
SHA-512 512 Digest::SHA v4.3.1 19.2
Whirlpool 512 Digest::Whirlpool v1.0.2 13.0
MD2 128 Digest::MD2 v2.03 9.5
Adler-32 32 Digest::Adler32 v0.03 1.3
CRC-16 16 Digest::CRC v0.05 1.1
CRC-32 32 Digest::CRC v0.05 1.1
MD5 128 Digest::Perl::MD5 v1.5 1.0
CRC-CCITT 16 Digest::CRC v0.05 0.8
ผมควานหา package อื่นๆที่เกี่ยวข้องหมดแล้วพบคำสั่งเข้ารหัส Apache MD5 อยู่ใน libcrypt-passwdmd5-perl แต่นั่นเป็น Crypt::PasswdMD5 ไม่ใช่ Digest ซึี่งหากจะใช้คำสั่งนี้ ผมจะต้องเข้าไปแก้ Bugzilla ซึ่งเป็นวิธีที่ไม่น่าทำสักเท่าไรนัก (แต่จริงๆแล้วก็อยากจะลองดูเหมือนกันนะ ☺) จึงต้องตัดสินใจลดระดับความปลอดภัยลงมาเป็น SHA-1