การเลือกตั้งเมื่อวันที่ 24 มีนาคม ที่ผ่านมาหลาย ๆ คนน่าจะเคยได้ดูการรายงานผลแบบ Real-time ของ Workpoint News ที่เว็บ https://vote.workpointnews.com ซึ่งจัดทำโดยทีม Cleverse ของเรา บทความนี้ จะมาเล่าถึงเบื้องหลังของการออกแบบโครงสร้าง และสถาปัตยกรรมของระบบ ไปจนถึงปัญหาต่าง ๆ ที่เจอกันระหว่างการทำงาน
(ขายของ: ใครอยากทำงานมันส์ๆ แบบนี้ – เรารับสมัครทั้ง Product Manager, Designer, Developer – สนใจมาได้ที่ https://cleverse.com/joinus)
จุดเริ่มต้น
พอได้ยินว่าทาง Workpoint News อยากทำเว็บไซต์แสดงผลการนับคะแนนเลือกตั้งที่จะถึง ทีมงานก็สนใจกันมาก เพราะเป็น project ที่นอกจากจะได้ปล่อยของด้าน design และ data visualization แล้ว ยังมี social impact ในเชิงการเป็นส่วนหนึ่งของการเลือกตั้งครั้งประวัติศาสตร์ครั้งหนึ่งของไทยอีกด้วย เราก็มีไอเดียกันหลากหลายก่อนที่จะเริ่มคุยงานกับทาง Workpoint ซะอีก โดยเราประเมินว่าใช้เวลาประมาณ 1 อาทิตย์ในการทำ project นี้
การออกแบบ
ทีมเราประเมินว่า traffic ที่จะเข้ามานั้นต้องสูงมากแน่ ๆ จึงพยายามออกแบบระบบให้สามารถรับ traffic ปริมาณมากได้ เราได้ออกแบบให้เว็บไซต์ทั้งหมดเป็นแบบ static files คือไม่มีการ run code ฝั่ง backend ใด ๆ เลย ดังนั้นข้อมูลทุกอย่างที่เราได้มาจาก API จึงต้องนำมา process ให้อยู่ในรูปแบบที่ frontend สามารถนำไปแสดงผลได้เลย เราเรียก process นี้ว่าการ ETL (extract-transform-load)
API ที่เราไปดึงข้อมูลมา เรียกว่า dttpool เป็นการรวมกันของสื่อต่าง ๆ เพื่อดึงข้อมูลจากกกต. ซึ่งจะช่วยลดการเรียกข้อมูลจากกกต.ให้น้อยที่สุด เพื่อป้องกันการล่ม (แต่มันก็ล่มมมม)
เราเขียน ETL ขึ้นมาให้รันอยู่บน Cloud Function ซึ่งจะถูก trigger โดย Cloud Scheduler ทุก ๆ หนึ่งนาที เมื่อเราดึงข้อมูลมา ETL เสร็จแล้ว ผลลัพธ์ออกมาเป็น JSON file โดยวางไฟล์ไว้ที่ Google Cloud Storage
และเราก็เพิ่งเปิด source code ของระบบ ETL เราสด ๆ ร้อน ๆ เข้าไปดูกันได้ที่ https://github.com/Cleverse/thailand-election-2019-etl
สำหรับในส่วนของ frontend ก็เป็นแบบ static เช่นเดียวกัน ทำหน้าที่แค่ load JSON file ที่เก็บไว้มาแสดงผล โดยเราวางไฟล์ไว้ที่ Firebase Hosting เพราะรองรับการ serve file แบบ single-page application การทำแบบนี้ทำให้การเปิดเว็บแต่ละครั้งนั้น ไม่มีการ run backend code ใด ๆ ทั้งสิ้นเลย
การ serve static files นั้นถึงแม้ว่าจะมีความรวดเร็วและกิน load server น้อยมากที่สุด แต่ถ้ามี traffic ปริมาณมหาศาลเข้ามา เราก็เดี้ยงได้เหมือนกัน ดังนั้นเราจึงต้องใช้สิ่งที่เรียกว่า CDN เข้ามาช่วยลด load ของเรา โดย CDN จะทำหน้าที่ cache static files ต่าง ๆ ไว้ที่แต่ละ node เพื่อความรวดเร็วในการโหลดไฟล์ ซึ่งเราเลือกใช้ CDN ที่มี node อยู่ตาม ISP ต่าง ๆ ในไทย เป็น product ของคนไทยเองชื่อว่า ByteArk โดยเอามาคั่นระหว่างผู้ใช้ กับ Firebase Hosting และ Google Cloud Storage ซึ่งเราประเมินไว้ว่า JSON file ของเราจะไม่ใหญ่มาก ผู้ใช้หลักแสนคน จะเสียค่าบริการไม่เยอะมากเท่าไหร่
ภาพรวมถือเป็นการออกแบบโดยใช้ Serverless Architecture โดยสามารถเขียนเป็น diagram ได้ดังนี้
การคำนวณส.ส.บัญชีรายชื่อ
สำหรับในส่วนของการคำนวณส.ส.บัญชีรายชื่อนั้น ตอนที่ได้ดู spec API จาก dttpool เราไม่ค่อยแน่ใจว่าเค้าจะส่งมาให้เราหรือเปล่า เราจึงตัดสินใจคำนวณด้วยตัวเอง ซึ่งเลยต้องมานั่งหารัฐธรรมนูญมาเปิดอ่าน โดยเขียนโปรแกรมคำนวณด้วย TypeScript พร้อมทั้งเปิดเป็น open source เพื่อให้คนอื่นสามารถนำไปใช้ และช่วยกันปรับปรุงได้ ซึ่งก็มีคนเข้ามาช่วยพัฒนา library ของเราจริง ๆ ด้วย และมีสื่อหลายเจ้าที่ใช้โปรแกรมคำนวณของเรา สามารถเข้าไปเยี่ยมชม repository ของเราบน GitHub ได้ที่ https://github.com/Cleverse/thailand-party-list-calculator
ก่อนวันเลือกตั้ง
ช่วง 2 วันก่อนถึงวันเลือกตั้งนั้นเป็นอะไรที่พีคมาก เรียกได้ว่ามีทีมงานทำงานตลอด 24 ชั่วโมง เปลี่ยนกะกันไปเรื่อย ๆ โดยความกดดันคือ deadline และการที่มีชื่อเสียงของ Cleverse เป็นเดิมพัน งานที่เคยประเมินว่าใช้หนึ่งอาทิตย์นั้นเกินจากที่คาดไปเกือบสองเท่าเลยทีเดียว สาเหตุหลัก ๆ เป็นเพราะเรามีทั้ง interaction, animation และ responsiveness ที่ต้องเก็บรายละเอียด ซึ่งก่อนถึงวันเลือกตั้ง dttpool จะมีการทำ dry run โดยการ simulate ข้อมูล random มาให้ทดลองแสดงผลก่อน ซึ่งเราก็สามารถแสดงผลได้อย่างไม่มีปัญหา
วันเลือกตั้ง
เราสังเกตว่าหน้าเว็บของเรายังไม่ได้ clear ข้อมูล จึงเข้าใจว่า dttpool ยังไม่ได้ reset data ซึ่งจริง ๆ แล้ว เป็นที่ทางเราเองที่ไม่ได้กรองข้อมูลที่ไม่มีคะแนนออกก่อน แล้วค่อย sort คนที่ได้ลำดับที่หนึ่ง ทำให้ขึ้นชื่อผู้สมัครอันดับหนึ่งที่มีคะแนนเป็น 0 ก็จึงทำการแก้ไขกันไป และก็ได้พบอีกปัญหาว่า เราไม่เคยทดสอบการที่เว็บไม่มีข้อมูลมาก่อน การ hover บนเขตที่ยังไม่มีคะแนนจะเกิด error ในฝั่ง frontend ทำให้เกิดหน้าขาว ถือเป็น case ที่ไม่เกิดขึ้นจังหวะที่ทำการ dry run
จังหวะที่คะแนนเริ่มเข้ามา และหน้าเว็บเริ่มเปลี่ยนจากสีเทาโล่ง ๆ เป็นสี ๆ ตามเขตตามพื้นที่แล้วรู้สึกฟินมาก เหมือนว่าความพยายามที่ทำกันมาเกือบ 2 อาทิตย์นั้นออกผลเป็นรูปธรรมซักที จากนั้นเราก็เริ่มทำการ monitor และปรับแก้ไขระบบตามปัญหาต่าง ๆ ที่มี report เข้ามา
อาจจะฟังดูเหมือนนักการเมือง แต่เราสามารถบอกได้ว่า ระบบของเราไม่ล่มเลย* เห็นดอกจันไหมครับ แปลว่ามีเงื่อนไขในการตีความคำว่าล่ม 5555 ล่มในที่นี้คือการที่มี traffic ปริมาณมากจน server handle ไม่ไหว สาเหตุที่ไม่ล่มเลยเพราะว่าเราใช้ static site และมี CDN คั่นอยู่ด้านหน้า อย่างไรก็ตาม มีบางช่วงที่ user ไม่สามารถเข้าใช้งานเว็บไซต์ได้ ซึ่งเกิดจากหลายสาเหตุด้วยกัน
- บางช่วงเจอหน้าขาว เกิดจากการที่เรา deploy version ใหม่แล้ว index.html ที่ CDN cache อยู่นั้นชี้ไปยัง asset เก่า แก้ไขได้โดยการ purge cache จาก CDN
- บางช่วงเจอ nginx welcome page หรือ certificate มีปัญหา อันนี้เกิดจาก misconfiguration ของ CDN
- บางช่วงเจอคะแนนผลแสดงผิดพลาด จำนวนส.ส.บัญชีรายชื่อรวมเกินโควต้า 150 คน อันนี้เราผิดเต็ม ๆ เพราะว่าใช้ library คำนวณ partylist version เก่า
เมื่อเกิดปัญหาแต่ละครั้ง จะมีคนแจ้งเข้ามาทุกช่องทาง เรียกได้ว่าสายไหม้เลยทีเดียว และทุก ๆ การ deploy หนึ่งครั้งจะต้องเข้าไป trigger เพื่อให้เกิดการ run cloud function ใหม่ และก็ต้องเข้าไป purge cache อีกด้วย ถือว่าค่อนข้างมี deploy cycle ที่ค่อนข้างช้า ไม่ทันการต่อการแก้ไขข้อมูลที่ผิดพลาด ตรงนี้เราควรจะมี kill switch เอาไว้ปิดทั้งระบบเมื่อมีข้อมูลผิดพลาด เพราะการไม่แสดงผลอะไรเลย ยังดีกว่าการแสดงผลข้อมูลผิดพลาดจากการคำนวณของเราเอง
สร้างจุดต่างที่โดดเด่น
ผลตอบรับที่ได้รับนั้นถือว่าเกินคาดมาก ซึ่งพอมาเทียบดูแล้ว เราพบว่าสิ่งหนึ่งที่เราทำต่างจากเว็บอื่นนั้นก็คือเว็บของเรามีรูปผู้สมัครส.ส.ทุกเขต จริง ๆ แล้วรูปผู้สมัครนั้น สื่อทุกเจ้าได้เหมือนกันหมด แต่ความยากคือคุณภาพของรูปนั้นจัดอยู่ในเกณฑ์ที่ไม่ค่อยดี ไม่พร้อมกันการนำมาแสดงผล โดยเป็นรูปแนวตั้งขนาด 600×952 โดยมีตัวอย่างของรูปที่มีปัญหาดังภาพด้านล่าง
จะเห็นได้ว่าถ้าเอารูปด้านบนไปแสดงผลตรง ๆ เว็บจะเละแน่นอน สิ่งที่เราทำคือเขียนโปรแกรม face detection เพื่อตรวจจับโครงหน้าในรูปทั้งหมด แล้วตัดเฉพาะสี่เหลี่ยมจตุรัสเพื่อให้สะดวกต่อการนำมาแสดงผล แต่ก็ยังมีบางภาพที่มีปัญหาบ้าง ก็ต้องเอามาแก้แบบ manual อีกทีนึง
ภายหลังวันเลือกตั้ง
เลือกตั้งจบ แต่งานยังไม่จบ มีกระแสเข้ามาว่าข้อมูลที่เผยแพร่ตามสื่อต่าง ๆ มีการขยับขึ้นลงแบบมั่วมาก ซึ่งข้อมูลทั้งหมดนั้นเราก็เอามาจาก dttpool ซึ่งเอามาจากกกต.อีกทีนั่นแหละ แต่เราก็ไม่กล้าฟันธงได้ว่าเป็นความผิดพลาดจาก hop ไหนของ data flow
พอมาดูยอดการใช้งาน CDN ก็พบกว่าผิดคาดไปมาก โดยมี data transfer ถึง 7TB ในวันที่ 24 มีนาคม ถือว่าเกินจากที่ประเมินไว้ไปเยอะมากกก แต่เมื่อเทียบกับ traffic ที่มีผู้คนจำนวนมากที่เลือกใช้งานเว็บของเราหลักล้านคน ก็ถือว่าคุ้มค่าทีเดียว
และเมื่อวันก่อนที่กกต.ประกาศผลผู้ชนะในแต่ละเขต ความพีคอยู่ที่คะแนนดิบที่ได้มาจากกกต.นั้นไม่ตรงกับผู้ชนะที่กกต.ประกาศ โดยเขตที่เป็นแบบนี้มีอยู่ 4 เขตด้วยกัน (FYI: 2 ใน 4 เป็นพรรคเพื่อไทย ที่กกต.ประกาศว่าชนะ) เราจึงต้องปรับแก้ โดยไม่ให้กระทบกับคะแนนดิบ และต้องนำมาคำนวณส.ส.บัญชีรายชื่อใหม่ ผลจึงเป็นอย่างที่เห็นในรูป
ตามรูปด้านบนจะเห็นว่าคะแนนพรรคพลังประชารัฐมากกว่าพรรคอนาคตใหม่ แต่ว่ากกต.ประกาศว่าพรรคอนาคตใหม่ชนะ เพราะว่าคะแนนดิบที่เห็นนั้นยังไม่นับไม่ครบ 100% จึงต้องทำการ override อันดับ
โชคดีที่ระบบ ETL ที่ออกแบบมา ทำให้เราแค่ override แบบ hardcode ที่เดียว และการคำนวณผลถัด ๆ มา เช่นส.ส.เขตรวม การคำนวณส.ส.บัญชีรายชื่อ ก็จะเป็นไปตามที่ override ไว้
และล่าสุด กกต.ประกาศคะแนนดิบมาแล้วแบบ 100% เป็นรูปแบบ PDF จ้าาา ก็ต้องมาหาทางแปลงให้ระบบนำไปใช้ต่อได้ น่าจะได้เห็นการเปลี่ยนแปลงข้อมูลเร็ว ๆ นี้
สรุป
การตัดสินใจรับทำงานนี้ ได้อะไรเยอะมาก ตั้งแต่ได้ออกแบบเพื่อรองรับผู้ใช้จำนวนมาก ได้ลองเล่น D3.js ในการ visualize ข้อมูล ได้ผลตอบรับที่ดีมาก ๆ จากผู้ใช้ ถือได้ว่าเป็นประสบการณ์ดี ๆ ที่ได้ทำงานในเสกลแบบนี้ ถึงแม้ว่าเวลาที่เราประเมินไว้ตอนแรกว่าหนึ่งอาทิตย์ จะใช้เวลาเกินไปเกือบสองเท่าก็ตาม
และที่สำคัญสุด ๆ คือต้องให้เครดิตทีมงาน Cleverse ทุกคนที่อดหลับอดนอน ทำให้ project นี้สำเร็จแบบถล่มทลายขนาดนี้