ตั้ง MQTT QoS สูงแล้ว คำสั่งจะไม่ซ้ำเลยหรือ

เวลาเอา MQTT มาสั่งอุปกรณ์ เรามักเริ่มอยากได้ความมั่นใจมากกว่าตอนส่งค่าเซนเซอร์ เพราะข้อความหนึ่งอาจหมายถึงเปิดพัดลมหรือให้กลไกทำงาน ถ้าเห็นตัวเลือก QoS สูงขึ้นก็ดูเหมือนน่าจะช่วยให้คำสั่งถึงและทำงานถูกต้องขึ้นได้พร้อมกัน
แต่เส้นทางของคำสั่งมีหลายจุดครับ ตั้งแต่แอปส่งเข้า broker ไปจนถึงอุปกรณ์รับ ตรวจเงื่อนไข และสั่งส่วนที่ทำงานจริง การยืนยันในช่วงหนึ่งจึงยังไม่ได้ตอบทุกอย่างที่เกิดหลังจากนั้น
ก่อนเลือก QoS แอดอยากชวนแยกว่าระบบต้องยืนยันอะไร และหากคำสั่งเดิมกลับมาอีกครั้ง เราต้องการให้ปลายทางทำอย่างไร เพราะโจทย์สองข้อนี้ยังต้องมีการออกแบบในแอป แม้เลือกวิธีส่งที่รัดกุมขึ้นแล้ว
QoS ดูแลการส่งข้อความในช่วงของมัน
QoS 0, 1 และ 2 มีวิธีดูแลการส่งและการตอบรับต่างกัน โดย QoS 1 เปิดโอกาสให้ข้อความถูกส่งซ้ำได้ ส่วน QoS 2 มีขั้นตอนเพื่อส่งข้อความครั้งเดียวในขอบเขตการรับส่งที่ protocol ดูแล แต่ไม่ได้เป็นธุรกรรมครอบคลุมฐานข้อมูล กลไก และโลกภายนอกของแอป
ในระบบที่มี broker เราต้องดูทั้งช่วงจากผู้ส่งไป broker และช่วงจาก broker ไปผู้รับ รวมถึงระดับที่ปลายทางสมัครรับกับความสามารถของระบบด้วย การตั้งค่าฝั่งส่งอย่างเดียวจึงยังไม่ใช่หลักฐานว่าข้อความผ่านทุกช่วงในเงื่อนไขที่ต้องการ

ถ้าแอปส่งข้อมูลซ้ำเป็นคำขอใหม่เอง ข้อความทั้งสองก็อาจถูกส่งถูกต้องตาม QoS ได้ทั้งคู่ เพราะ protocol ไม่ได้รู้ว่าคนกดปุ่มสองครั้งหรือโปรแกรมกำลังลองงานเดิมอีกรอบ ดังนั้นคำว่า exactly once ในการส่งจึงไม่ควรถูกขยายความว่าอุปกรณ์จะทำผลทางธุรกิจครั้งเดียวเสมอไป
สั่งให้เป็นสถานะกับสั่งให้ทำเพิ่มต่างกัน
ลองเทียบคำสั่งว่าให้ไฟอยู่ในสถานะเปิด กับคำสั่งให้สลับไฟหนึ่งครั้ง ถ้าคำสั่งเปิดถูกประมวลผลซ้ำ ผลสุดท้ายอาจยังเป็นเปิดเหมือนเดิม แต่ถ้าสลับสองรอบก็อาจกลับมาปิด จึงควรเลือกความหมายของคำสั่งให้เหมาะกับสิ่งที่ต้องควบคุมตั้งแต่แรก
คำสั่งเพิ่มจำนวนหรือให้กลไกเดินอีกหนึ่งรอบก็มีลักษณะเดียวกัน การทำซ้ำไม่เท่ากับการส่งสถานะเดิมอีกครั้ง เราต้องรู้ว่าคำขอใดเป็นงานเดิมและงานใดเป็นความตั้งใจรอบใหม่ของผู้ใช้ ไม่อาศัยการเทียบเพียง payload ว่าเหมือนกันหรือไม่
วิธีหนึ่งคือให้คำสั่งมี command_id ที่สร้างตั้งแต่ผู้ขอเริ่มงาน และใช้รหัสเดิมตลอดการลองส่งงานนั้นใหม่ หากสร้างรหัสใหม่ทุกครั้งที่ timeout ปลายทางก็จะแยกไม่ออกว่ารอบไหนเป็น retry ของงานเดิมหรือเป็นงานใหม่จริงๆ
ปลายทางต้องจำว่าเคยรับงานไหน
เมื่อมีรหัสแล้ว ฝั่งอุปกรณ์หรือบริการปลายทางก็ต้องตรวจรหัสก่อนทำงาน พร้อมมีสถานะที่อธิบายได้ เช่น รับแล้ว กำลังทำ สำเร็จ หรือปฏิเสธตามเงื่อนไข ถ้ารหัสเดิมกลับมา อาจตอบสถานะเดิมแทนการเริ่มผลลัพธ์ซ้ำอีกครั้ง

ที่เก็บรหัสต้องมีขอบเขตด้วย จะจำไว้กี่รายการ นานเท่าไร และอยู่ข้ามการรีสตาร์ตหรือไม่ ถ้าจำใน RAM อย่างเดียว เมื่อบอร์ดเริ่มใหม่ก็อาจลืมว่างานนั้นเคยทำแล้ว ส่วนการเก็บถาวรก็ต้องคิดเรื่องความทนทานของข้อมูลและพื้นที่ตามอุปกรณ์
รหัสระดับ MQTT ที่ protocol ใช้ภายในไม่ควรถูกนำมาแทนรหัสงานระยะยาวโดยตรง เพราะมันมีขอบเขตและการนำกลับมาใช้ตามระบบรับส่ง ไม่ได้ถูกออกแบบให้เป็นหมายเลขของคำสั่งธุรกิจที่ทุกบริการอ้างข้ามเวลาได้ตามใจเรา
เมื่อหลายบริการเข้าถึงงานเดียวกัน การตรวจซ้ำกับบันทึกการรับงานควรถูกจัดให้สอดคล้องกันด้วย หากสองงานอ่านพร้อมกันว่ายังไม่เคยทำ แล้วต่างเริ่มทำทั้งคู่ การมีช่อง command_id ใน payload เพียงอย่างเดียวก็ยังไม่ช่วยป้องกันผลซ้ำ
จุดที่ไฟดับอาจทำให้ผลไม่แน่ชัด
สมมติอุปกรณ์สั่งกลไกทำงานสำเร็จแล้ว แต่ไฟดับก่อนบันทึกผล เมื่อกลับมารับคำสั่งเดิม มันอาจยังไม่รู้ว่างานเกิดขึ้นไปแล้ว ในทางกลับกัน ถ้าบันทึกสำเร็จก่อนลงมือจริงแล้วไฟดับ ก็อาจมีประวัติที่บอกว่าทำแล้วทั้งที่ผลยังไม่เกิด
จุดนี้ทำให้เห็นว่าผลทางกายภาพกับข้อมูลในระบบอาจไม่ได้บันทึกเป็นชุดเดียวกันได้ง่ายๆ งานที่มีผลสำคัญจึงควรมีวิธีตรวจสถานะจริง ขั้นยืนยันเพิ่มเติม หรือการจัดการกรณีไม่แน่ใจตามความเหมาะสม ไม่แก้ด้วยการเพิ่ม QoS แล้วถือว่าปัญหาหายทั้งหมด
การตอบว่าไม่ทราบผลแน่ชัดอาจเป็นสถานะที่จำเป็น ถ้าระบบยังไม่มีหลักฐานพอ การรีบตอบว่าสำเร็จหรือเริ่มทำใหม่อัตโนมัติอาจทำให้ผู้ใช้เข้าใจผิด ควรกำหนดตั้งแต่ต้นว่ากรณีใดลองใหม่ได้และกรณีใดต้องตรวจผลก่อน
รายงานผลให้ตรงกับสิ่งที่ยืนยันได้
ฝั่งหน้าจอควรแยกว่าส่งคำขอแล้ว อุปกรณ์รับแล้ว และงานสำเร็จแล้ว เพราะแต่ละสถานะมีหลักฐานต่างกัน หากแสดงว่าสำเร็จทันทีที่ publish ผ่าน ผู้ใช้ก็อาจไม่รู้ว่าปลายทางปฏิเสธหรือยังรอทำงานอยู่
ผลตอบกลับควรอ้าง command_id เดียวกับคำขอ พร้อมรายละเอียดเท่าที่จำเป็น เช่น เวลาประมวลผล สถานะ และเหตุผลที่ทำไม่ได้ เพื่อให้หน้าจอจับคู่ผลได้ถูก แม้มีหลายคำสั่งกำลังรอพร้อมกัน หรือผลเก่ามาถึงหลังคำสั่งใหม่
ส่วน timeout ฝั่งผู้ขอควรหมายถึงยังไม่ได้รับผลในเวลาที่กำหนด ไม่ใช่ข้อยืนยันว่างานไม่เกิด การลองใหม่ต้องยึดรหัสเดิมและนโยบายที่ตกลงไว้ รวมถึงอาจมีช่องทางถามสถานะของรหัสนั้นก่อนเริ่มความพยายามรอบใหม่
ในการทดลองควรจำลองช่วง reconnect การส่งซ้ำ และการเริ่มระบบใหม่อย่างตั้งใจ แล้วตรวจทั้งข้อความกับผลที่อุปกรณ์ทำจริง การเห็น broker ตอบรับครบเพียงอย่างเดียวยังเป็นการทดสอบบางส่วนของเส้นทางเท่านั้น
ควรกำหนดอายุคำสั่งร่วมด้วย หากอุปกรณ์กลับมาเชื่อมต่อหลังจากเวลาที่การสั่งนั้นยังมีประโยชน์ผ่านไปแล้ว การทำคำสั่งเก่าถูกต้องครบหนึ่งครั้งก็อาจยังไม่ตรงความต้องการของผู้ใช้ ส่วนอายุข้อความใน protocol กับเวลาที่แอปยอมทำงานต้องถูกตรวจให้สอดคล้องกัน โดยดูเวลาของอุปกรณ์และเงื่อนไขที่เชื่อถือได้ด้วย
สรุป
QoS ช่วยกำหนดความน่าเชื่อถือของการส่ง MQTT แต่ความหมายว่าคำสั่งสำเร็จและทำซ้ำได้หรือไม่ยังเป็นงานของแอป เราจึงควรออกแบบคำสั่ง รหัสงาน ที่เก็บสถานะ และวิธีตรวจผลให้ไปด้วยกัน เมื่อหน้าจอแยกการส่ง การรับ และผลจริงได้ชัด ก็จะตามปัญหาได้ตรงกว่าการหวังว่าระดับ QoS ที่สูงขึ้นจะดูแลทุกอย่างให้เองครับ


