บันทึกการ Upgrade MySQL 8.3 เป็น 8.4 จนเว็บพังกับ กับ mysql_native_password

เอาอีกแล้วครับ รอบก่อนตอน Upgrade จาก MySQL 5.7 เป็น 8.0 ก็ทำเอาปวดหัวชิบหายกันไปแล้ว ผ่านไปอีก 2 ปี เรื่องตลกมันกลับมาอีกแล้ว แต่ใครจะคิดละครับว่า Upgrade จาก 8.3 เป็น 8.4 ที่เป็น Minor Version จะทำให้เว็บนี้แตกยับ ๆ ไปเกือบวันเต็ม ๆ ได้ มันเกิดอะไรขึ้น และเราแก้ปัญหายังไง มาดูกัน

กด Upgrade สักหน่อย (ไม่)เสียหาย

Setup ของเว็บ arnondora.in.th ที่ใช้งานอยู่ตอนนี้คือ เรามี Ghost CMS และ MySQL 8.3 ที่รันผ่าน Docker อยู่บน Synology NAS ที่บ้านของเรา และต่อออกไปข้างนอกบ้านผ่าน Cloudflare Tunnel ออกไป

เรื่องของเรื่องที่เกิดคือ เช้าวันหนึ่ง เราเปิด Admin ของ Synology NAS ออกมา และเห็นว่า MySQL Image ที่เราใช้งานอยู่มันมี Update ด้วยความไม่ได้คิดอะไรมาก เราก็กด Update ไปเลยจ๊ะ แล้วเดินไปอาบน้ำ กะว่า ออกมา ก็ Upgrade เสร็จพอดีแล้วมา Touch-up อะไรเล็ก ๆ น้อย ๆ ก็น่าจะใช้เวลาไม่นานมาก ไม่ถึง 10 นาที ก็จบละ

ปรากฏว่าอาบน้ำแต่งตัวเสร็จ ออกมาดูที่ Console ปรากฏว่า Container มัน Shutdown เฉยเลย แล้วมี Notification ว่า Database Container Unexpected Shutdown เอาแล้วไง มันเกิดอะไรขึ้นกันนะ ปรากฏว่า เราเข้าไปดูใน Log มันบอกว่า Data Upgrade Failed อะไรสักอย่าง เราไม่ได้แคปจอเก็บไว้ จนมาอ่านดี ๆ คือ เดี๋ยวนะ !!!! มันบอกว่า Data Upgrade from MySQL 8030 to 9000 Failed อะไรสักอย่าง

เลยไปดูที่ Image Tag ที่เราใช้งาน ชัดเลย Latest Tag เจ้าค่ะ พอ Update ไป ตัวล่าสุด ณ วันที่เกิดเรื่องคือ 9.0 แมร่งเอ้ย พึ่งเขียนข่าวเรื่องนี้อยู่เลย ทำไมลืมได้วะ คุ้น ๆ ว่ามันมี Breaking Change อะไรสักอย่างแน่ ๆ เลยทำให้มันไม่สามารถ Automatics Migrate ตรง ๆ ได้

ไหน ๆ ไป 9.0 ยังไม่ได้แล้ว ขอไป 8.x ที่ไกลที่สุดละกัน

เราเลยคิดว่า โอเค ถ้าเราอยากจะไป 9.0 หรือว่า เราจะต้องไป 8.0 ที่ไกลที่สุดก่อน ซึ่งไปดูใน Image Tag มา ก็คือมันอยู่ที่ 8.4.4 เราก็เอาเลยครับ Download Image Tag 8.4.4 แล้วรันเลย ซึ่ง Log ออกมาก็ดูดีเลย คือ มันเจอแล้วว่า Data Files อันเก่ามันเป็น 8.3 แล้วจะทำการ Upgrade Data Dictionary กับ Data File เป็น Scheme ของ Version 8.4 แต่ด้วยความที Database มันรันอยู่บน HDD เลยน่าจะทำให้นานละมั้ง จนผ่านไปสักพักใหญ่ ๆ ใน Log ของ MySQL 8.4.4 Container ก็บอกว่า Ready for connection แปลว่า มันพร้อมใช้งานได้แล้ว

เราเลย Start Ghost CMS ขึ้นมา ปรากฏว่า น้องเขา Unexpected Shutdown แว่บแรกคือ คิดได้อย่างเดียวว่ามันต่อ Database ไม่ติด มัน Authentication ไม่ผ่านเหรอ ไม่น่าละมั้ง จนเข้าไปดูใน Log ปรากฏว่า มันบอก Database Connection Failed แล้วไล่ Trace ลงไปมันบอกว่า mysql_native_password is not loaded ห่ะ อะไรวะ มันคืออะไร๊

เพื่อเป็นการเช็ค Database เราเลยเอา DataGrip พยายาม Login เข้าไปใน Database ปรากฏว่า มันบอก Error เดียวกันคือ mysql_native_password is not loaded เหมือนกัน แปลว่า Error นี้ไม่ได้เป็นของ Ghost CMS แต่เป็นของ MySQL แล้วละ สรุปคือ เราเข้าไปทำอะไร Database ไม่ได้เลย

กลับตัวก็ไม่ได้ ให้เดินต่อไปก็ไปไม่ถึง

ตอนนั้นเลยคิดว่า งั้นเอางี้ ในเมื่อเราไป 8.4.4 ไม่ได้ งั้นเราย้อนกลับไปที่ 8.3 เหมือนเดิมก่อนละกัน ใช้ Data File เดิมและสร้าง Container ด้วย Image ของ MySQL 8.3 ปรากฏว่า พอ Start ขึ้นมา เจอ Unexpected Shutdown อีกแล้ว ใน Log บอกว่า มันเจอว่า Data File ของเรามันเป็นของ Version 8.4 และมันไม่ยอมให้ Downgrade ของมาที่ 8.3 ได้ แล้วมันก็ Shutdown ใส่แบบไร้เยื่อใยมาก ๆ อ้าวชิบหายไง ก็เคว้งเลยสิทีนี้

แต่ ๆๆๆๆ คิดว่านายอานนท์จะเจ็บแล้วไม่จำจริง ๆ เหรอ ผ่านความชิบหายมาเยอะขนาดนี้แล้ว นี่เลย เรามี Backup ของ Data File ทุกคืนเว้ย โหวว แค่นี้กระจอก แค่เราดึง Data File ที่ Backup ไว้เมื่อคืนลงมา แล้วเราก็สร้าง Container ของ MySQL 8.3 มาจับ มันก็ต้องใช้ได้สิว๊าาาา ปรากฏว่า ใน Log นางมีอะไรบางอย่างเกี่ยวกับ Data Restoration Process อ่าน ๆ ไป เหมือนมันบอกว่า ไฟล์อะไรสักอย่างหายไป (จริง ๆ มันบอกนะ แต่เราจำไม่ได้แล้ว) และการ Restore มันเอากลับมาได้บางส่วน แต่บาง Table มันใช้งานไม่ได้เลย โชคดีว่า Table ที่สำคัญ ๆ เก็บบทความและข้อมูล Tag ต่าง ๆ มันอยู่ใช้งานได้ทั้งหมด

เราเลย Dump Data เท่าที่ได้ออกมา และไปสร้าง Container ของ MySQL 8.3 ขึ้นมาใหม่ แล้วให้ Ghost CMS มัน Migrate Database เพื่อให้เราได้ Structure แล้ว Shutdown ใส่มันไปเลย จากนั้นเราก็ไล่ Truncate ข้อมูลทั้งหมด แล้วโยนข้อมูลที่ Dump ออกมาได้ใส่กลับเข้าไป เว็บก็กลับมารันได้ แต่ความพีคคือ มันมีบทความล่าสุด 2 อันที่หายไป งง ไปเลยทีนี้ เพราะเราสั่ง Daily Backup แล้วทำไมมันหายไปได้ ก็ไม่รู้เหมือนกัน เท่ากับว่าบทความนั้น หายไปตลอดกาล

ปัญหา MySQL 8.4 มันเกิดจากอะไร ?

หลังจากเราไปหาข้อมูลมา ก็คือ เขาบอกว่า ใน MySQL 9.0 เขาจะเอา วิธีการ Authenticate ตัวเดิมอย่าง mysql_native_password ออกไป และแทนที่ด้วย Default ใหม่อย่าง caching_sha2_password โดยใน Version 8.4 เขาจะเริ่มปรับผู้ใช้ โดยการปิด mysql_native_password เป็นค่า Default ทำให้เมื่อเรา Upgrade ไปใช้ 8.4.4 เราเลยเจอปัญหาว่า mysql_native_password is not loaded นั่นเอง

# Enable mysql_native_password plugin
[mysqld]
mysql_native_password=ON

โดยใน Version 8.4 เขาแค่ปิดไว้ไม่ได้เปิดมาให้แต่แรก เราสามารถสั่งให้มันเปิดการใช้งานได้ โดยการเข้าไปเขียน Config เพิ่มเติม แล้วเอาไปไว้ใน /etc/mysql/conf.d/ หากเราใช้ Docker Container เราก็แค่เขียน Config นี้แล้วสั่ง Mount ลงไปใน Path ที่เราบอกไว้เลยก็ได้

ALTER USER '<USERNAME>'@'<HOST>' IDENTIFIED WITH caching_sha2_password BY '<PASSWORD>';

และเมื่อเราเข้าใช้งาน Database ได้แล้ว ให้เรา Migrate จาก mysql_native_password ไปเป็น caching_shr2_password โดยการรัน SQL Command ด้านบน แล้วเปลี่ยน Username, Host และ Password ใหม่

select user,host, plugin from mysql.user;

จากนั้นให้เราเช็คดูอีกทีด้วย SQL Command ด้านบน โดยเราจะต้องเห็นว่า User ทุกตัวที่มีในระบบ Field Plugin จะต้องไม่มี mysql_native_password แล้ว เราถึงจะชัวร์ว่า เราสามารถ Upgrade ไปที่ MySQL 8.4 และ MySQL 9.0 ได้นั่นเอง

จากเรื่องนี้ เราปรับปรุงอะไรบ้าง ?

จากเหตุการณ์ที่เกิดขึ้นครั้งนี้ เป็นอีกครั้งที่ทำให้เรามาปรับปรุง Process การจัดการ Software Dependencies ของเว็บ arnondora.in.th อีกครั้ง โดยมีการพุ่งเป้าไปที่ 2 ส่วน

ส่วนที่ 1 คือ ระบบการสำรองข้อมูล จากเดิมเราใช้ Hyper Backup ดึงเอา Data File จาก MySQL โดยตรงออกไปเลย ซึ่งเราไม่แน่ใจว่ามันเกิดอะไรขึ้น เลยทำให้เมื่อเราเอาไฟล์ออกมารัน มันกลับใช้งานไม่ได้ เราเดาว่า ณ เวลาที่ Backup มันไล่ไปที่ละไฟล์แล้วปรากฏว่า มันมีการเขียน Record บางอย่าง เลยทำให้พอเราเอามาเปิดรัน มันเลยใช้งานไม่ได้ เราเลยทำให้มันถูกกฏหมายมากขึ้นละกัน ด้วยการใช้ mysqldump ดึงข้อมูลออกมา และหากเกิดเหตุการณ์อะไรขึ้น เราไม่ต้องมานั่งเครียดแล้วว่ามัน Version ไหน เราแค่เปิด Container ของสัก Version แล้ว Restore Dump File เข้าไป ซึ่งเราก็จะเขียน Command เข้าไปรัน mysqldump ทุกคืนผ่าน crontab ออกมาเป็นไฟล์เก็บไว้ และเราสั่งให้ Hyper Backup สำรอง Dump File เอาไว้อีกที่ก็เป็นอันเรียบร้อย

ส่วนที่ 2 คือ การ Upgrade Dependencies จากเดิม เราไม่ได้สนใจอะไรเท่าไหร่ ก็กด Upgrade ไปเลย จนทำให้ชิบหายกันมาหลายดอกแล้ว ถ้าใครที่ติดตามเรามานาน ก็น่าจะรู้วีรกรรมของเรากันดี คือถามว่ารู้มั้ยว่ามันต้องทำอะไรยังไง ก็รู้นะ แต่เหมือนเรา Take it for granted มานานว่ามันเว็บเราเองรันอยู่ในบ้านตัวเอง ไม่ต้อง Setup อะไรอลังการมากหรอก แต่พอตอนนี้มันมีคนเข้าเยอะขึ้น มี Content เยอะขึ้น การเดิมพันมันสูงขึ้น เราควรจะต้อง Setup Process อะไรบางอย่างเพื่อป้องกันเรื่องพวกนี้แล้ว

สิ่งที่เราทำคือ เราเขียน Pipeline ง่าย ๆ บน Gitlab CI ที่เรารันอยู่ในบ้านอยู่แล้ว (ชั่ยครับ บ้านเรามี Git Server พร้อม CI/CD จริง ๆ) โดยการที่เราจะสร้าง Repo ที่มีไฟล์ Tag ของ Ghost และ MySQL อยู่ แล้วเมื่อมันมีการ Update เราจะสั่งให้มันรัน Pipeline ที่มันจะไปเรียก Terraform สำหรับการสร้าง VM บน Synology NAS ขึ้นมาเป็น Ubuntu Image ที่เราเตรียมเอาไว้ จากนั้น เราจะสั่งให้มัน SSH เข้าไปเพื่อ Copy Website Content จาก Production และ Database Dump File ล่าสุดใส่เข้าไป และทำการสร้าง MySQL Container โดยใช้ Image Tag ตามที่กำหนดไว้ พร้อมกับ Config ที่เหมือน Production แล้ว Restore Database โดยใช้ mysql Command ทั่ว ๆ ไป และสุดท้ายก็สร้าง Ghost CMS Container ขึ้นมา โดยใช้ Image Tag ที่เรากำหนดใน Config File เอาไว้

จากนั้น เราก็ทำการ Health Check ทั้ง MySQL และ Ghost CMS ว่าทำงานได้หรือไม่ หากมี Container ใดที่ Shutdown ก็คือให้ Fail ทันที จากนั้นเราจะให้มันเข้าไป Query Database ออกมา หากมี Error ก็จะให้ Fail และสุดท้าย เราใช้ curl ในการดึงหน้าแรก และหน้าอื่น ๆ แบบสุ่มทั้งหมด 50 หน้า แล้วออกมาว่า มันทำได้เหมือนกับหน้า Production จริงหรือไม่ หากมีหน้าใดหน้าหนึ่งที่ไม่ตรง ก็จะให้ Fail ทันทีเช่นกัน ดังนั้น การเช็คเราจะเช็คทั้งหมด 3 ส่วนคือ การทำงานของ Container, Database Connectivity และ Integration Test ของทั้งสองส่วนร่วมกัน

API documentation | Portainer Documentation

หากทุกส่วนรัน Passed ทั้งหมด เราจะสั่งให้มันไปเรียก API ของ Portainer ให้ Shutdown Container ของ Ghost และ MySQL เดิมแล้ว แล้วเปลี่ยนชื่อให้ลงท้ายด้วย _bak เพื่อไว้ก่อน จากนั้นให้สร้าง Container อันใหม่ที่เป็น Version ตามที่เราทดสอบผ่านออกมา สั่ง Start แล้วเช็คอีกรอบว่า มันทำงานได้ตามที่ควรจะเป็น 3 อย่างหรือไม่ จากนั้นถ้าผ่าน ค่อยลบ Container ที่ลงท้ายด้วย _bak ทิ้งไป แต่ถ้าไม่ผ่าน เราจะให้มันลบ Container ใหม่ แล้วเปลี่ยนชื่อตัวที่ลงท้ายด้วย _bak กลับมาที่เดิม แล้วค่อย Start มันกลับขึ้นมา สุดท้ายเมื่อการทดสอบเสร็จสิ้นเราก็จะบอกให้ Destroy VM ทิ้งไปเป็นอันทำลายหลักฐาน

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

สรุป

จากปัญหาที่เกิดขึ้น ยอมรับเลยว่ามันเป็นเพราะเรา Take it for granted ว่าไม่ต้องทำอะไรมันมากหรอก รัน ๆ แบบบ้าน ๆ ไปนี่แหละ แต่พอครั้งนี้มันทำให้เรารู้ว่า เราทำแบบเดิมไม่ได้แล้ว เราไม่ได้มีเวลามานั่งดูแล และแก้ปัญหาเยอะแล้ว ประกอบกับเว็บก็ใหญ่มากขึ้นเรื่อย ๆ มีคนเข้าเยอะกว่าเดิมมาก ๆ การหายไปไม่กี่นาที ก็คือคนเยอะมาก ๆ แล้ว ก็หวังว่าบทเรียนในครั้งนี้น่าจะทำให้ เรา และ ใครหลาย ๆ คนได้เรียนรู้อะไรไปบ้างไม่น้อย ก็น้อยนะ และหวังว่า Pipeline ที่เรานั่งเขียนอยู่ 2 ชั่วโมง นี้จะแก้ปัญหาให้เราแบบ Once and for all สักทีนะ กราบละ !!