בהקשר להתפתחות המהירה של ארכיטקטורת מחשוב ענן וארכיטקטורת מיקרו-שירותים, יש צורך הולך וגובר לספק יכולת ביצוע דינמי של קוד לשפות תכנות שונות עם הבטחה לאבטחה, מדרגיות וביצועים גבוהים. מאמר זה מתאר פרויקט המיישם ביצוע קוד בסביבה מבודדת, ודן ביתרונות של הפתרון הארכיטקטוני הנבחר עבור WEB IDE מודרני. המערכת בנויה על
נתאר בהרחבה כיצד בנויים המרכיבים העיקריים של המערכת, במה הם שונים מפתרונות חלופיים ומדוע הבחירה בטכנולוגיות אלו מאפשרת השגת ביצועים ואבטחה גבוהים.
הפרויקט בנוי על העיקרון של ארכיטקטורת מיקרו-שירותים, המאפשר לחלק את הפונקציונליות לשירותים עצמאיים. כל רכיב אחראי על משימה מיוחדת מאוד, המבטיחה גמישות, מדרגיות וסובלנות תקלות של המערכת.
מרכיבים עיקריים:
היתרונות של Go:
ביצועים ומדרגיות: ל-Go יש מהירות ביצוע גבוהה, שחשובה במיוחד לטיפול במספר רב של בקשות מקבילות.
תמיכת מקבילות מובנית: המנגנונים של גורוטיינים וערוצים מאפשרים יישום אינטראקציה אסינכרונית בין רכיבים ללא דפוסי ריבוי השחלות מורכבים.
היתרונות של gRPC:
השוואה: בניגוד ל-REST API, gRPC מספק תקשורת יעילה ואמינה יותר בין שירותים, שהיא קריטית עבור מערכות במקביל.
למה Redis?
ביצועים גבוהים: Redis יכול להתמודד עם מספר רב של פעולות בשנייה, מה שהופך אותו לאידיאלי לתורי משימות ותוצאות.
תמיכה ב-Pub/Sub ו-List: הפשטות של הטמעת תורים ומנגנוני מנוי מקלה על ארגון אינטראקציות אסינכרוניות בין שירותים.
השוואה למתווכים אחרים של הודעות: בניגוד ל-RabbitMQ או Kafka, Redis דורש פחות תצורה ומספק ביצועים מספיקים למערכות בזמן אמת.
תפקידו של דוקר:
בידוד סביבה: קונטיינרים של Docker מאפשרים להריץ קוד בסביבה מבודדת לחלוטין, מה שמגביר את בטיחות הביצוע ומפחית את הסיכון להתנגשויות עם המערכת הראשית.
יכולת ניהול ועקביות: שימוש ב-Docker מספק את אותה סביבה להידור וביצוע קוד, ללא קשר למערכת המארחת.
השוואה: הפעלת קוד ישירות על המארח יכולה להוות סיכון אבטחה ולהוביל להתנגשויות תלות, בעוד Docker מאפשר לך לפתור את הבעיות הללו.
פרויקט זה משתמש בגישת מיקרו-שירות, שיש לה מספר יתרונות משמעותיים:
בעת בניית WEB IDEs מודרניים לביצוע קוד מרחוק, לעתים קרובות משווים פתרונות ארכיטקטוניים שונים. הבה נבחן שתי גישות:
גישה א': ארכיטקטורת Microservice (gRPC + Redis + Docker)
תכונות:
גישה זו מספקת תקשורת בין-שירותית מהירה ואמינה, בידוד גבוה של ביצוע קוד ושינוי קנה מידה גמיש עקב קונטיינריזציה. זה מושלם עבור WEB IDEs מודרניים, שבהם היענות ואבטחה חשובים.
גישה ב': ארכיטקטורה מונוליתית מסורתית (HTTP REST + ביצוע מרכזי)
תכונות:
פתרונות מונוליטיים, המשמשים לעתים קרובות בגרסאות מוקדמות של IDEs אינטרנט, מבוססים על HTTP REST וביצוע קוד מרכזי. מערכות כאלה מתמודדות עם בעיות קנה מידה, חביון מוגבר וקשיים בהבטחת אבטחה בעת ביצוע קוד של מישהו אחר.
הערה: בהקשר המודרני של פיתוח WEB IDE, גישת ה-HTTP REST וביצוע ריכוזי נחותים מהיתרונות של ארכיטקטורת מיקרו-שירותים, מכיוון שהיא אינה מספקת את הגמישות וההרחבה הדרושים.
הגרף מראה בבירור שארכיטקטורת המיקרו-שירותים (גישה A) מספקת חביון נמוך יותר, תפוקה גבוהה יותר, אבטחה ומדרגיות טובה יותר בהשוואה לפתרון המונוליטי (גישה ב').
אחד המרכיבים המרכזיים של אבטחת ויציבות המערכת הוא השימוש ב-Docker. בפתרון שלנו, כל השירותים פרוסים בקונטיינרים נפרדים, מה שמבטיח:
להלן גרסה ממוזערת של חלקי הקוד העיקריים המדגימה כיצד המערכת:
המערכת משתמשת ברישום גלובלי, שבו לכל שפה יש רץ משלה. זה מאפשר לך להוסיף בקלות תמיכה בשפות חדשות, זה מספיק כדי ליישם את ממשק הרץ ולרשום אותו:
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)
ומקבל את הרץ המתאים לביצוע הקוד.
עבור כל בקשת קוד משתמש, נוצר קונטיינר נפרד של 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, כאשר:
לפיכך, כאשר קוראים לשיטת ה-Run של הרץ, קורה הדבר הבא:
קטע ממוזער של ההיגיון הראשי של ביצוע קוד (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 } }
בדוגמה המינימלית הזו:
קטעי מפתח אלו מראים כיצד המערכת תומכת בהרחבה (הוספה קלה של שפות חדשות) ומספקת בידוד על ידי יצירת קונטיינר Docker נפרד עבור כל בקשה. גישה זו משפרת את האבטחה, היציבות והסקלביליות של הפלטפורמה, מה שחשוב במיוחד עבור WEB IDEs מודרניים.
מאמר זה דן בפלטפורמה לביצוע קוד מרחוק הבנויה על ארכיטקטורת מיקרו-שירות באמצעות מחסנית gRPC + Redis + Docker. גישה זו מאפשרת לך:
ניתוח השוואתי מראה כי ארכיטקטורת המיקרו-שירות עולה בהרבה על פתרונות מונוליטיים מסורתיים בכל מדדי המפתח. היתרונות של גישה זו מאושרים על ידי נתונים אמיתיים, מה שהופך אותה לפתרון אטרקטיבי ליצירת מערכות בעלות ביצועים גבוהים וסובלנות תקלות.
מחבר: אולקסי בונדר
תאריך: 2025–02–07