ในบริบทของการพัฒนาอย่างรวดเร็วของการประมวลผลบนคลาวด์และสถาปัตยกรรมไมโครเซอร์วิส มีความต้องการเพิ่มขึ้นในการจัดเตรียมความสามารถในการเรียกใช้โค้ดแบบไดนามิกสำหรับภาษาการเขียนโปรแกรมต่างๆ พร้อมการรับประกันความปลอดภัย ความสามารถในการปรับขนาด และประสิทธิภาพสูง บทความนี้จะอธิบายโครงการที่นำการเรียกใช้โค้ดไปใช้ในสภาพแวดล้อมที่แยกจากกัน และหารือถึงข้อดีของโซลูชันสถาปัตยกรรมที่เลือกสำหรับ WEB IDE สมัยใหม่ ระบบนี้สร้างขึ้นบน
เราจะอธิบายรายละเอียดว่าส่วนประกอบหลักของระบบมีโครงสร้างอย่างไร แตกต่างจากโซลูชันทางเลือกอย่างไร และเหตุใดการเลือกเทคโนโลยีเหล่านี้จึงช่วยให้มีประสิทธิภาพและความปลอดภัยสูงได้
1. ภาพรวมสถาปัตยกรรมและส่วนประกอบหลัก
โครงการนี้สร้างขึ้นบนหลักการของสถาปัตยกรรมไมโครเซอร์วิส ซึ่งช่วยให้คุณแบ่งฟังก์ชันการทำงานออกเป็นบริการอิสระได้ แต่ละส่วนประกอบมีหน้าที่รับผิดชอบงานเฉพาะทางสูง ซึ่งรับประกันความยืดหยุ่น ความสามารถในการปรับขนาด และความทนทานต่อข้อผิดพลาดของระบบ
ส่วนประกอบหลัก:
- gRPC ใช้สำหรับการสื่อสารระหว่างบริการ เหมาะอย่างยิ่งสำหรับการถ่ายโอนข้อมูลระหว่างไมโครเซอร์วิส เนื่องจาก:
- โปรโตคอลไบนารี (โปรโตคอลบัฟเฟอร์): รับประกันการถ่ายโอนข้อมูลที่รวดเร็วและกะทัดรัด
- การพิมพ์ที่เข้มงวด: ช่วยหลีกเลี่ยงข้อผิดพลาดในการถ่ายโอนและประมวลผลข้อมูล
- ความหน่วงต่ำ: ซึ่งเป็นสิ่งสำคัญสำหรับการโทรภายในระหว่างบริการ (เช่น ระหว่างเซิร์ฟเวอร์ gRPC และคิว Redis)
- เซิร์ฟเวอร์ WebSocket: ให้การสื่อสารสองทางกับไคลเอนต์เพื่อส่งผลลัพธ์การดำเนินการแบบเรียลไทม์ เซิร์ฟเวอร์จะสมัครรับข้อมูลผลลัพธ์และส่งต่อข้อมูลไปยังไคลเอนต์ ทำให้แสดงบันทึกการคอมไพล์และการดำเนินการได้ทันที
- เวิร์กเกอร์: บริการอิสระที่ดึงงานจากคิว สร้างสภาพแวดล้อมการทำงานชั่วคราว ตรวจสอบและดำเนินการรหัสในคอนเทนเนอร์ Docker ที่แยกจากกัน แล้วเผยแพร่ผลลัพธ์ของการดำเนินการกลับไปยังคิว
- Redis: ใช้เป็นโบรกเกอร์ข้อความเพื่อโอนงานจากเซิร์ฟเวอร์ gRPC ไปยัง Worker และผลลัพธ์จาก Worker ไปยังเซิร์ฟเวอร์ WebSocket ข้อดีของ Redis คือความเร็วสูง รองรับ Pub/Sub และปรับขนาดได้ง่าย
- โมดูลภายใน:
- คอมไพเลอร์และ Docker Runner: โมดูลที่รับผิดชอบในการรันคำสั่ง Docker พร้อมการบันทึกสตรีม ช่วยให้สามารถตรวจสอบกระบวนการคอมไพเลอร์และการดำเนินการได้แบบเรียลไทม์
- Language Runners: ผสมผสานตรรกะสำหรับการตรวจสอบ การคอมไพล์ และการดำเนินการของโค้ดสำหรับภาษาต่างๆ (C, C++, C#, Python, JavaScript, TypeScript) แต่ละโปรแกรมรันเนอร์ใช้อินเทอร์เฟซเดียว ซึ่งทำให้การขยายฟังก์ชันสำหรับภาษาใหม่ๆ ง่ายขึ้น
2. เทคโนโลยีและเหตุผลในการเลือก
ไป
ข้อดีของ Go:
ประสิทธิภาพและความสามารถในการปรับขนาด: Go มีความเร็วในการดำเนินการสูง ซึ่งมีความสำคัญอย่างยิ่งสำหรับการจัดการคำขอขนานจำนวนมาก
การรองรับการทำงานพร้อมกันในตัว: กลไกของ goroutines และช่องทางช่วยให้สามารถใช้งานการโต้ตอบแบบอะซิงโครนัสระหว่างส่วนประกอบต่างๆ ได้โดยไม่ต้องใช้รูปแบบมัลติเธรดที่ซับซ้อน
จีอาร์พีซี
ข้อดีของ gRPC:
- การถ่ายโอนข้อมูลที่มีประสิทธิภาพ: ด้วยโปรโตคอลการถ่ายโอนข้อมูลไบนารี (Protocol Buffers) gRPC จึงให้ความหน่วงเวลาต่ำและโหลดเครือข่ายต่ำ
- การพิมพ์ที่เข้มงวด: ช่วยลดจำนวนข้อผิดพลาดที่เกี่ยวข้องกับการตีความข้อมูลที่ไม่ถูกต้องระหว่างไมโครเซอร์วิส
- รองรับการสตรีมแบบทิศทางสองทาง: ซึ่งมีประโยชน์อย่างยิ่งสำหรับการแลกเปลี่ยนบันทึกและผลลัพธ์การดำเนินการแบบเรียลไทม์
การเปรียบเทียบ: แตกต่างจาก REST API, gRPC มอบการสื่อสารระหว่างบริการที่มีประสิทธิภาพและเชื่อถือได้มากกว่า ซึ่งเป็นสิ่งสำคัญสำหรับระบบที่มีการทำงานพร้อมกันสูง
เรดิส
เหตุใดจึงเลือก Redis?
ประสิทธิภาพสูง: Redis สามารถจัดการการทำงานจำนวนมากต่อวินาที ซึ่งทำให้เหมาะอย่างยิ่งสำหรับคิวงานและผลลัพธ์
การรองรับ Pub/Sub และรายการ: ความเรียบง่ายของการนำคิวและกลไกการสมัครรับข้อมูลมาใช้ทำให้สามารถจัดระเบียบการโต้ตอบแบบอะซิงโครนัสระหว่างบริการต่างๆ ได้อย่างง่ายดาย
การเปรียบเทียบกับโบรกเกอร์ข้อความอื่น ๆ: ไม่เหมือน RabbitMQ หรือ Kafka, Redis ต้องมีการกำหนดค่าน้อยกว่าและมอบประสิทธิภาพเพียงพอสำหรับระบบเรียลไทม์
ด็อกเกอร์
บทบาทของ Docker:
การแยกสภาพแวดล้อม: คอนเทนเนอร์ Docker ช่วยให้คุณสามารถเรียกใช้โค้ดในสภาพแวดล้อมที่แยกออกจากกันโดยสมบูรณ์ ซึ่งจะเพิ่มความปลอดภัยในการดำเนินการและลดความเสี่ยงของการขัดแย้งกับระบบหลัก
การจัดการและความสอดคล้องกัน: การใช้ Docker มอบสภาพแวดล้อมแบบเดียวกันในการคอมไพล์และดำเนินการโค้ด โดยไม่คำนึงถึงระบบโฮสต์
การเปรียบเทียบ: การรันโค้ดโดยตรงบนโฮสต์อาจก่อให้เกิดความเสี่ยงด้านความปลอดภัยและนำไปสู่ความขัดแย้งในการอ้างอิง ในขณะที่ Docker ช่วยให้คุณแก้ไขปัญหาเหล่านี้ได้
เว็บซ็อกเก็ต
- แบบเรียลไทม์: การเชื่อมต่ออย่างต่อเนื่องกับไคลเอนต์ช่วยให้สามารถถ่ายโอนข้อมูล (บันทึก ผลการดำเนินการ) ได้ทันที
- ประสบการณ์ผู้ใช้ที่ได้รับการปรับปรุง: ด้วย WebSocket IDE สามารถแสดงผลลัพธ์ของโค้ดแบบไดนามิกได้
3. ประโยชน์ของสถาปัตยกรรมไมโครเซอร์วิส
โครงการนี้ใช้แนวทางไมโครเซอร์วิสซึ่งมีข้อดีสำคัญหลายประการ:
- การปรับขนาดอิสระ: สามารถปรับขนาดบริการแต่ละอย่าง (เซิร์ฟเวอร์ gRPC, Worker, เซิร์ฟเวอร์ WebSocket, Redis) ได้แยกกันตามโหลด ซึ่งช่วยให้ใช้ทรัพยากรได้อย่างมีประสิทธิภาพและปรับตัวได้รวดเร็วตามจำนวนคำขอที่เพิ่มขึ้น
- การทนทานต่อข้อผิดพลาด: การแบ่งระบบออกเป็นโมดูลอิสระหมายความว่าความล้มเหลวของไมโครเซอร์วิสหนึ่งรายการจะไม่ส่งผลให้ระบบทั้งหมดล้มเหลว ซึ่งจะช่วยเพิ่มเสถียรภาพโดยรวมและทำให้การกู้คืนจากข้อผิดพลาดง่ายขึ้น
- ความยืดหยุ่นในการพัฒนาและปรับใช้: ไมโครเซอร์วิสได้รับการพัฒนาและปรับใช้โดยอิสระ ซึ่งทำให้การแนะนำฟีเจอร์และการอัปเดตใหม่ทำได้ง่ายขึ้น นอกจากนี้ยังช่วยให้คุณสามารถใช้เทคโนโลยีที่เหมาะสมที่สุดสำหรับแต่ละบริการเฉพาะได้อีกด้วย
- ความสะดวกในการบูรณาการ: อินเทอร์เฟซที่กำหนดไว้อย่างชัดเจน (เช่น ผ่าน gRPC) ทำให้สามารถเชื่อมต่อบริการใหม่ได้อย่างง่ายดายโดยไม่ต้องเปลี่ยนแปลงสถาปัตยกรรมที่มีอยู่มากนัก
- การแยกและการรักษาความปลอดภัย: ไมโครเซอร์วิสแต่ละรายการสามารถทำงานในคอนเทนเนอร์ของตัวเอง ซึ่งจะช่วยลดความเสี่ยงที่เกี่ยวข้องกับการรันโค้ดที่ไม่ปลอดภัย และให้การป้องกันอีกชั้นหนึ่ง
4. การวิเคราะห์เชิงเปรียบเทียบแนวทางสถาปัตยกรรม
เมื่อสร้าง WEB IDE ที่ทันสมัยสำหรับการรันโค้ดจากระยะไกล มักมีการเปรียบเทียบโซลูชันทางสถาปัตยกรรมต่างๆ ลองพิจารณาสองแนวทาง:
แนวทาง A: สถาปัตยกรรมไมโครเซอร์วิส (gRPC + Redis + Docker)
- ความหน่วง: 40 มิลลิวินาที
- ปริมาณงาน: 90 หน่วย
- ระบบรักษาความปลอดภัย : 85 ยูนิต
- ความสามารถในการขยาย: 90 หน่วย
คุณสมบัติ:
แนวทางนี้ช่วยให้การสื่อสารระหว่างบริการรวดเร็วและเชื่อถือได้ การแยกการทำงานของโค้ดออกจากกันสูง และการปรับขนาดที่ยืดหยุ่นเนื่องจากการทำคอนเทนเนอร์ เหมาะอย่างยิ่งสำหรับ WEB IDE ยุคใหม่ ซึ่งการตอบสนองและความปลอดภัยเป็นสิ่งสำคัญ
แนวทาง B: สถาปัตยกรรมโมโนลิธิกแบบดั้งเดิม (HTTP REST + การดำเนินการแบบรวมศูนย์)
- ความหน่วง: 70 มิลลิวินาที
- ปริมาณงาน: 65 หน่วย
- ระบบรักษาความปลอดภัย : 60 ยูนิต
- ความสามารถในการขยาย: 70 หน่วย
คุณสมบัติ:
โซลูชันแบบโมโนลิธิก ซึ่งมักใช้ในเวอร์ชันแรกของ IDE เว็บนั้นใช้ HTTP REST และการรันโค้ดแบบรวมศูนย์ ระบบดังกล่าวจะประสบปัญหาด้านการปรับขนาด ความล่าช้าที่เพิ่มขึ้น และความยากลำบากในการรับรองความปลอดภัยเมื่อรันโค้ดของผู้อื่น
หมายเหตุ: ในบริบทสมัยใหม่ของการพัฒนา WEB IDE แนวทาง HTTP REST และการดำเนินการแบบรวมศูนย์นั้นด้อยกว่าข้อดีของสถาปัตยกรรมไมโครเซอร์วิส เนื่องจากไม่ได้ให้ความยืดหยุ่นและความสามารถในการปรับขนาดตามที่จำเป็น
การแสดงภาพของเมตริกเชิงเปรียบเทียบ
กราฟแสดงให้เห็นอย่างชัดเจนว่าสถาปัตยกรรมไมโครเซอร์วิส (แนวทาง A) ให้ความหน่วงที่ต่ำกว่า ปริมาณงานที่สูงขึ้น ปลอดภัยกว่า และมีความสามารถในการปรับขนาดได้ดีกว่าเมื่อเปรียบเทียบกับโซลูชันแบบโมโนลิธิก (แนวทาง B)
5. สถาปัตยกรรม Docker: การแยกและความสามารถในการปรับขนาด
องค์ประกอบสำคัญประการหนึ่งของการรักษาความปลอดภัยและเสถียรภาพของระบบคือการใช้ Docker ในโซลูชันของเรา บริการทั้งหมดจะถูกปรับใช้ในคอนเทนเนอร์ที่แยกจากกัน ซึ่งช่วยให้แน่ใจได้ว่า:
- การแยกสภาพแวดล้อมการทำงาน: บริการแต่ละอย่าง (เซิร์ฟเวอร์ gRPC, Worker, เซิร์ฟเวอร์ WebSocket) และโบรกเกอร์ข้อความ (Redis) ทำงานในคอนเทนเนอร์ของตัวเอง ซึ่งช่วยลดความเสี่ยงของโค้ดที่ไม่ปลอดภัยที่จะส่งผลกระทบต่อระบบหลัก ในขณะเดียวกัน โค้ดที่ผู้ใช้เรียกใช้ในเบราว์เซอร์ (เช่น ผ่าน WEB IDE) จะถูกสร้างและดำเนินการในคอนเทนเนอร์ Docker แยกต่างหากสำหรับแต่ละงาน แนวทางนี้ช่วยให้มั่นใจได้ว่าโค้ดที่อาจไม่ปลอดภัยหรือผิดพลาดจะไม่ส่งผลกระทบต่อการทำงานของโครงสร้างพื้นฐานหลัก
- ความสอดคล้องของสภาพแวดล้อม: การใช้ Docker ช่วยให้แน่ใจว่าการตั้งค่ายังคงเหมือนเดิมในสภาพแวดล้อมการพัฒนา การทดสอบ และการผลิต ซึ่งช่วยลดความซับซ้อนของการดีบักและทำให้แน่ใจถึงความสามารถในการคาดเดาการทำงานของโค้ด
- ความยืดหยุ่นในการปรับขนาด: สามารถปรับขนาดแต่ละส่วนประกอบได้อย่างอิสระ ซึ่งช่วยให้คุณปรับตัวให้เข้ากับโหลดที่เปลี่ยนแปลงได้อย่างมีประสิทธิภาพ ตัวอย่างเช่น เมื่อจำนวนคำขอเพิ่มขึ้น คุณสามารถเปิดใช้คอนเทนเนอร์ Worker เพิ่มเติมได้ โดยแต่ละคอนเทนเนอร์จะสร้างคอนเทนเนอร์แยกกันสำหรับการดำเนินการโค้ดของผู้ใช้
6. ส่วนเล็ก ๆ ของโค้ด
ด้านล่างนี้เป็นเวอร์ชันย่อของส่วนหลักของโค้ดที่แสดงให้เห็นถึงการทำงานของระบบ:
- กำหนดภาษาที่จะรันโดยใช้รีจิสทรีตัวรันระดับโลก
- เริ่มคอนเทนเนอร์ Docker เพื่อรันโค้ดผู้ใช้โดยใช้ฟังก์ชัน RunInDockerStreaming
1. การตรวจจับภาษาผ่านการลงทะเบียนนักวิ่ง
ระบบใช้รีจิสทรีทั่วโลก โดยที่แต่ละภาษาจะมีตัวรันเนอร์ของตัวเอง วิธีนี้ทำให้คุณสามารถเพิ่มการรองรับภาษาใหม่ได้อย่างง่ายดาย เพียงแค่คุณนำอินเทอร์เฟซตัวรันเนอร์ไปใช้งานและลงทะเบียน:
package languages import ( "errors" "sync" ) var ( registry = make(map[string]Runner) registryMu sync.RWMutex ) type Runner interface { Validate(projectDir string) error Compile(ctx context.Context, projectDir string) (<-chan string, error) Run(ctx context.Context, projectDir string) (<-chan string, error) } func Register(language string, runner Runner) { registryMu.Lock() defer registryMu.Unlock() registry[language] = runner } func GetRunner(language string) (Runner, error) { registryMu.RLock() defer registryMu.RUnlock() if runner, exists := registry[language]; exists { return runner, nil } return nil, errors.New("unsupported language") }
ตัวอย่างการลงทะเบียนภาษาใหม่:
func init() { languages.Register("python", NewGenericRunner("python")) languages.Register("javascript", NewGenericRunner("javascript")) }
ดังนั้นเมื่อได้รับคำขอระบบจะเรียกใช้งานดังนี้
runner, err := languages.GetRunner(req.Language)
และรับตัวรันเนอร์ที่สอดคล้องเพื่อดำเนินการโค้ด
2. การเปิดตัวคอนเทนเนอร์ Docker เพื่อดำเนินการโค้ด
สำหรับคำขอรหัสผู้ใช้แต่ละราย จะมีการสร้างคอนเทนเนอร์ Docker แยกต่างหาก ซึ่งจะดำเนินการภายในเมธอดรันเนอร์ (ตัวอย่างเช่น ใน Run) ตรรกะหลักในการรันคอนเทนเนอร์อยู่ในฟังก์ชัน RunInDockerStreaming:
package compiler import ( "bufio" "fmt" "io" "log" "os/exec" "time" ) func RunInDockerStreaming(image, dir, cmdStr string, logCh chan < -string) error { timeout: = 50 * time.Second cmd: = exec.Command("docker", "run", "--memory=256m", "--cpus=0.5", "--network=none", "-v", fmt.Sprintf("%s:/app", dir), "-w", "/app", image, "sh", "-c", cmdStr) cmd.Stdin = nil stdoutPipe, err: = cmd.StdoutPipe() if err != nil { return fmt.Errorf("error getting stdout: %v", err) } stderrPipe, err: = cmd.StderrPipe() if err != nil { return fmt.Errorf("error getting stderr: %v", err) } if err: = cmd.Start();err != nil { return fmt.Errorf("Error starting command: %v", err) } // Reading logs from the container go func() { reader: = bufio.NewReader(io.MultiReader(stdoutPipe, stderrPipe)) for { line, isPrefix, err: = reader.ReadLine() if err != nil { if err != io.EOF { logCh < -fmt.Sprintf("[Error reading logs: %v]", err) } break } msg: = string(line) for isPrefix { more, morePrefix, err: = reader.ReadLine() if err != nil { break } msg += string(more) isPrefix = morePrefix } logCh < -msg } close(logCh) }() doneCh: = make(chan error, 1) go func() { doneCh < -cmd.Wait() }() select { case err: = < -doneCh: return err case <-time.After(timeout): if cmd.Process != nil { cmd.Process.Kill() } return fmt.Errorf("Execution timed out") } }
ฟังก์ชันนี้จะสร้างคำสั่ง docker run โดยที่:
- image เป็นภาพ Docker ที่เลือกสำหรับภาษาเฉพาะ (กำหนดโดยการกำหนดค่าตัวรันเนอร์)
- dir คือไดเร็กทอรีที่มีรหัสที่สร้างขึ้นสำหรับคำขอนี้
- cmdStr คือคำสั่งสำหรับการคอมไพล์หรือดำเนินการโค้ด
ดังนั้นเมื่อเรียกใช้เมธอด Run ของตัวเรียกใช้งาน จะเกิดขึ้นต่อไปนี้:
- ฟังก์ชัน RunInDockerStreaming จะเริ่มคอนเทนเนอร์ Docker ที่มีการเรียกใช้โค้ด
- บันทึกการดำเนินการจะถูกสตรีมไปยังช่อง logCh ซึ่งช่วยให้คุณสามารถส่งข้อมูลเกี่ยวกับกระบวนการดำเนินการได้แบบเรียลไทม์
3. กระบวนการดำเนินการแบบบูรณาการ
ย่อส่วนของตรรกะหลักของการทำงานของโค้ด (executor.ExecuteCode):
func ExecuteCode(ctx context.Context, req CodeRequest, logCh chan string) CodeResponse { // Create a temporary directory and write files projectDir, err: = util.CreateTempProjectDir() if err != nil { return CodeResponse { "", fmt.Sprintf("Error: %v", err) } } defer os.RemoveAll(projectDir) for fileName, content: = range req.Files { util.WriteFileRecursive(filepath.Join(projectDir, fileName), [] byte(content)) } // Get a runner for the selected language runner, err: = languages.GetRunner(req.Language) if err != nil { return CodeResponse { "", err.Error() } } if err: = runner.Validate(projectDir); err != nil { return CodeResponse { "", fmt.Sprintf("Validation error: %v", err) } } // Compile (if needed) and run code in Docker container compileCh, _: = runner.Compile(ctx, projectDir) for msg: = range compileCh { logCh < -"[Compilation]: " + msg } runCh, _: = runner.Run(ctx, projectDir) var output string for msg: = range runCh { logCh < -"[Run]: " + msg output += msg + "\n" } return CodeResponse { Output: output } }
ในตัวอย่างขั้นต่ำนี้:
- การตรวจจับภาษาจะดำเนินการโดยเรียก languages.GetRunner(req.Language) ซึ่งช่วยให้สามารถเพิ่มการรองรับสำหรับภาษาใหม่ได้อย่างง่ายดาย
- การเปิดตัวคอนเทนเนอร์ Docker ถูกนำมาใช้ภายในวิธีการ Compile/Run ซึ่งใช้ RunInDockerStreaming เพื่อรันโค้ดแบบแยกส่วน
ส่วนสำคัญเหล่านี้แสดงให้เห็นว่าระบบรองรับการขยายได้ (การเพิ่มภาษาใหม่ได้ง่าย) อย่างไร และให้การแยกส่วนโดยการสร้างคอนเทนเนอร์ Docker แยกต่างหากสำหรับแต่ละคำขอ แนวทางนี้ช่วยปรับปรุงความปลอดภัย ความเสถียร และความสามารถในการปรับขนาดของแพลตฟอร์ม ซึ่งมีความสำคัญอย่างยิ่งสำหรับ WEB IDE สมัยใหม่
7. บทสรุป
บทความนี้จะกล่าวถึงแพลตฟอร์มสำหรับการเรียกใช้โค้ดจากระยะไกลที่สร้างขึ้นบนสถาปัตยกรรมไมโครเซอร์วิสโดยใช้สแต็ก gRPC + Redis + Docker แนวทางนี้ช่วยให้คุณสามารถ:
- ลดเวลาแฝงและรับรองปริมาณงานสูงเนื่องจากการสื่อสารระหว่างบริการที่มีประสิทธิภาพ
- รับประกันความปลอดภัยโดยแยกการทำงานของโค้ดในคอนเทนเนอร์ Docker แยกต่างหาก โดยจะสร้างคอนเทนเนอร์แยกกันสำหรับคำขอของผู้ใช้แต่ละราย
- การปรับขนาดระบบอย่างยืดหยุ่นเนื่องจากการปรับขนาดไมโครเซอร์วิสแบบอิสระ
- ส่งมอบผลลัพธ์แบบเรียลไทม์ผ่าน WebSocket ซึ่งมีความสำคัญอย่างยิ่งสำหรับ WEB IDE สมัยใหม่
การวิเคราะห์เชิงเปรียบเทียบแสดงให้เห็นว่าสถาปัตยกรรมไมโครเซอร์วิสมีประสิทธิภาพเหนือกว่าโซลูชันโมโนลิธิกแบบเดิมอย่างเห็นได้ชัดในเมตริกที่สำคัญทั้งหมด ข้อดีของแนวทางนี้ได้รับการยืนยันจากข้อมูลจริง ซึ่งทำให้เป็นโซลูชันที่น่าสนใจสำหรับการสร้างระบบที่มีประสิทธิภาพสูงและทนต่อความผิดพลาด
ผู้แต่ง : โอเล็กซี บอนดาร์
วันที่ : 2025-02-07