การให้คะแนนความเสี่ยงจากการฉ้อโกงแบบเรียลไทม์ในระดับสเกลใหญ่
tracio.ai ประมวลผล 50K events/วินาที พร้อมการให้คะแนนต่ำกว่า 50ms ได้อย่างไร ด้วย stream processing, signal vector ที่คำนวณล่วงหน้า และ edge caching
การให้คะแนนความเสี่ยงจากการฉ้อโกงในระดับสเกลใหญ่ต้องการสถาปัตยกรรมที่แตกต่างจากการประมวลผลแบบ batch โดยสิ้นเชิง เมื่อกำลังมีการอนุมัติการชำระเงินหรือการสร้างบัญชี คุณมีเวลาเพียงหลักมิลลิวินาที — ไม่ใช่หลักนาที — ในการส่งมอบคะแนนความเสี่ยง ที่ tracio.ai เราประมวลผลมากกว่า 50,000 events ต่อวินาที โดยมี latency ของการให้คะแนนที่ค่ามัธยฐาน 22ms บทความนี้จะอธิบายสถาปัตยกรรมที่ทำให้สิ่งนี้เป็นไปได้
ไปป์ไลน์การให้คะแนน
ทุก event ที่เข้ามาจะผ่านไปป์ไลน์สามขั้นตอน ได้แก่ การเสริมข้อมูลสัญญาณ (signal enrichment), การคำนวณ vector และการให้คะแนนความเสี่ยง การเสริมข้อมูลสัญญาณจะแนบข้อมูล device intelligence — ลายนิ้วมือ (fingerprint) ของผู้เยี่ยมชม, ผลการตรวจจับบอท, ข้อมูล IP intelligence และพฤติกรรมในอดีต — เข้ากับ event ดิบ การคำนวณ vector จะแปลงสัญญาณที่เสริมแล้วเหล่านี้ให้เป็น feature vector ความยาวคงที่ที่ปรับให้เหมาะกับโมเดลการให้คะแนนของเรา การให้คะแนนความเสี่ยงจะรัน vector ผ่านโมเดลที่ฝึกไว้ของเราและคืนค่าคะแนนระหว่าง 0.0 ถึง 1.0
การตัดสินใจด้านการออกแบบที่สำคัญคือการแยกการเสริมข้อมูลและการคำนวณ vector ออกจากการให้คะแนน ข้อมูลการเสริมจะถูกคำนวณล่วงหน้าและแคชไว้ เมื่อผู้เยี่ยมชมโหลดหน้าเว็บ เราจะคำนวณโปรไฟล์ของอุปกรณ์และเก็บไว้ใน Redis ด้วย TTL 60 นาที เมื่อคำขอให้คะแนนเข้ามา — โดยทั่วไปถูกกระตุ้นโดยการชำระเงินหรือการล็อกอิน — เราจะดึงโปรไฟล์ที่คำนวณล่วงหน้าไว้แทนที่จะคำนวณใหม่ ซึ่งช่วยลด latency ของการให้คะแนนจาก 200ms+ ลงเหลือต่ำกว่า 30ms
Stream Processing ด้วย Go
เลเยอร์การรับข้อมูลเข้าของเราเขียนด้วย Go และใช้สถาปัตยกรรมแบบ fan-out event ที่เข้ามาจะมาถึงผ่าน HTTP POST และถูกวางลงบน channel ภายในทันที พูล (pool) ของ worker goroutines จะอ่านจาก channel นี้ ทำการเสริมข้อมูล และเขียน event ที่เสริมแล้วลง ClickHouse สำหรับการวิเคราะห์ และลงคิวการให้คะแนนสำหรับการประมวลผลแบบเรียลไทม์ พูล fan-out จะปรับขนาดแบบไดนามิกตามความลึกของคิว
เราเลือก Go สำหรับเลเยอร์การรับข้อมูลเข้าเนื่องจากพรีมิทีฟด้านการทำงานพร้อมกัน (concurrency primitives) ที่ยอดเยี่ยมและการจัดสรรหน่วยความจำที่คาดเดาได้ worker goroutine แต่ละตัวใช้พื้นที่สแตกประมาณ 4KB ทำให้เราสามารถรัน worker พร้อมกันได้หลายพันตัวบนโหนดเดียว การหยุดชั่วคราวของ garbage collector ที่ต่ำกว่ามิลลิวินาทีมีความสำคัญอย่างยิ่งต่อการรักษา latency ให้สม่ำเสมอที่ throughput สูง
Edge Caching และ Signal Vector
สำหรับลูกค้าที่มีปริมาณสูงสุดของเรา เราจะ deploy โมเดลการให้คะแนนที่ edge โดยใช้แคช signal vector ที่คำนวณล่วงหน้า เมื่อพบอุปกรณ์เป็นครั้งแรก เราจะคำนวณ signal vector แบบเต็มของอุปกรณ์นั้นและเก็บไว้ในแคช edge ของเรา (deploy บน Cloudflare Workers KV) คำขอให้คะแนนถัดไปสำหรับอุปกรณ์เดียวกันจะดึง vector ที่แคชไว้และรันการให้คะแนนในเครื่องที่ edge ทำให้ได้ latency ต่ำกว่า 10ms
โมเดลการให้คะแนนที่ edge เป็นเวอร์ชันกลั่น (distilled) ของโมเดลเต็มของเรา — เล็กกว่าและเร็วกว่า แต่ปรับให้เหมาะกับเป้าหมายความแม่นยำเดียวกัน เราฝึกโมเดล edge ซ้ำทุกสัปดาห์และ deploy อัปเดตผ่าน rolling deployment เพื่อหลีกเลี่ยงพายุการทำให้แคชเป็นโมฆะ (cache invalidation storms) โมเดลเต็มจะรันฝั่งเซิร์ฟเวอร์สำหรับกรณีที่ค่าความเชื่อมั่นของโมเดล edge ต่ำกว่าเกณฑ์ที่กำหนดค่าได้
ClickHouse สำหรับการวิเคราะห์
event ที่เสริมข้อมูลแล้วทั้งหมดจะถูกเก็บไว้ใน ClickHouse ซึ่งเป็นฐานข้อมูลการวิเคราะห์แบบคอลัมน์ (columnar) ของเรา การบีบอัดและประสิทธิภาพการ query ของ ClickHouse ช่วยให้เราเก็บ event ได้หลายพันล้านรายการในขณะที่ยังรองรับ query เชิงวิเคราะห์แบบเรียลไทม์ ลูกค้าของเราใช้การวิเคราะห์เหล่านี้เพื่อทำความเข้าใจรูปแบบการฉ้อโกง ปรับเกณฑ์การให้คะแนน และสืบสวน event แต่ละรายการ
เราใช้ materialized views ใน ClickHouse เพื่อรักษาเมตริกที่รวมค่าไว้ล่วงหน้า ได้แก่ อัตราการฉ้อโกงตามประเทศ, การกระจายคะแนนตามประเภทอุปกรณ์ และอัตรา false positive ตามเกณฑ์ materialized views เหล่านี้จะอัปเดตแบบเรียลไทม์เมื่อ event เข้ามา ทำให้ได้เมตริกที่พร้อมสำหรับแดชบอร์ดโดยไม่ต้องใช้ query การรวมค่าที่มีต้นทุนสูง
บทเรียนที่ได้เรียนรู้
การสร้างระบบการให้คะแนนแบบเรียลไทม์สอนบทเรียนหลายอย่างแก่เรา ประการแรก การคำนวณล่วงหน้าคือการปรับให้เหมาะสมที่สำคัญที่สุด — งานใดก็ตามที่คุณสามารถทำได้ก่อนที่คำขอให้คะแนนจะมาถึง คืองานที่ไม่ถูกนับรวมในงบประมาณ latency ของคุณ ประการที่สอง โมเดลการทำงานพร้อมกันของ Go เหมาะกับการประมวลผล event ที่ throughput สูง แต่คุณต้องมีวินัยเรื่องการจัดสรรหน่วยความจำเพื่อหลีกเลี่ยงแรงกดดันจาก GC ประการที่สาม การ deploy ที่ edge เปลี่ยนแปลงเรื่อง latency อย่างพลิกโฉม แต่ต้องการการจัดการโมเดลอย่างรอบคอบเพื่อหลีกเลี่ยงการทำนายที่ล้าสมัย