paint-brush
Este IDE web ejecuta su código en la nube sin dañar su computadora portátilpor@oleksiijko
Nueva Historia

Este IDE web ejecuta su código en la nube sin dañar su computadora portátil

por Oleksii Bondar12m2025/02/21
Read on Terminal Reader

Demasiado Largo; Para Leer

El proyecto se basa en el principio de la arquitectura de microservicios, que permite dividir la funcionalidad en servicios independientes. Cada componente es responsable de una tarea altamente especializada, lo que garantiza la flexibilidad, la escalabilidad y la tolerancia a fallos del sistema. El proyecto se basa en el lenguaje de programación Go.
featured image - Este IDE web ejecuta su código en la nube sin dañar su computadora portátil
Oleksii Bondar HackerNoon profile picture
0-item
1-item

En el contexto del rápido desarrollo de la computación en la nube y la arquitectura de microservicios, existe una creciente necesidad de proporcionar la capacidad de ejecutar código de forma dinámica para varios lenguajes de programación con garantía de seguridad, escalabilidad y alto rendimiento. Este artículo describe un proyecto que implementa la ejecución de código en un entorno aislado y analiza las ventajas de la solución arquitectónica elegida para un IDE WEB moderno. El sistema está construido sobre Ir , usos gRPC para una interacción eficaz entre servicios, Redis como intermediario de mensajes y Estibador para aislar el entorno de ejecución. WebSocket El servidor se utiliza para mostrar resultados en tiempo real.

Describiremos en detalle cómo están estructurados los principales componentes del sistema, en qué se diferencian de las soluciones alternativas y por qué la elección de estas tecnologías permite alcanzar un alto rendimiento y seguridad.


1. Descripción general de la arquitectura y componentes principales

El proyecto se basa en el principio de la arquitectura de microservicios, que permite dividir la funcionalidad en servicios independientes. Cada componente es responsable de una tarea altamente especializada, lo que garantiza la flexibilidad, la escalabilidad y la tolerancia a fallas del sistema.


Componentes principales:


  • gRPC se utiliza para la comunicación entre servicios. Es ideal para transferir datos entre microservicios debido a:
    • Protocolo binario (Protocol Buffers): garantiza una transferencia de datos rápida y compacta.
    • Tipificación estricta: ayuda a evitar errores en la transferencia y el procesamiento de datos.
    • Baja latencia: lo cual es fundamental para las llamadas internas entre servicios (por ejemplo, entre un servidor gRPC y una cola Redis).
  • Servidor WebSocket: Proporciona comunicación bidireccional con el cliente para transmitir resultados de ejecución en tiempo real. Se suscribe a una cola con resultados y reenvía los datos al cliente, proporcionando visualización instantánea de registros de compilación y ejecución.
  • Trabajador: un servicio independiente que extrae tareas de una cola, crea un entorno de trabajo temporal, valida y ejecuta código en un contenedor Docker aislado y luego publica los resultados de la ejecución en la cola.
  • Redis: se utiliza como agente de mensajes para transferir tareas desde el servidor gRPC al Worker y resultados desde el Worker al servidor WebSocket. Las ventajas de Redis son la alta velocidad, la compatibilidad con Pub/Sub y la facilidad de escalado.
  • Módulos internos:
    • Compilador y Docker Runner: Módulo encargado de ejecutar comandos Docker con registro de flujo, permitiendo monitorizar en tiempo real el proceso de compilación y ejecución.
    • Ejecutores de lenguaje: combinan lógica para validación, compilación y ejecución de código para varios lenguajes (C, C++, C#, Python, JavaScript, TypeScript). Cada ejecutor implementa una única interfaz, lo que simplifica la expansión de funcionalidad para nuevos lenguajes.


El diagrama a continuación muestra el flujo de datos desde el cliente hasta el proceso de trabajo y viceversa mediante gRPC, Redis y WebSocket.


2. Tecnologías y justificación de la elección

Ir

Ventajas de Go:

  • Rendimiento y escalabilidad: Go tiene una alta velocidad de ejecución, lo que es especialmente importante para manejar una gran cantidad de solicitudes paralelas.

  • Soporte de concurrencia incorporado: Los mecanismos de goroutines y canales permiten implementar interacción asincrónica entre componentes sin patrones complejos de subprocesamiento múltiple.


gRPC

Ventajas de gRPC:

  • Transferencia de datos eficiente: gracias al protocolo de transferencia binaria (Protocol Buffers), gRPC proporciona baja latencia y baja carga de red.
  • Tipificación fuerte: esto reduce la cantidad de errores asociados con la interpretación incorrecta de datos entre microservicios.
  • Soporte para transmisión bidireccional: esto es especialmente útil para intercambiar registros y resultados de ejecución en tiempo real.

Comparación: a diferencia de la API REST, gRPC proporciona una comunicación más eficiente y confiable entre servicios, lo cual es fundamental para sistemas altamente concurrentes.


Redis

¿Por qué Redis?

  • Alto rendimiento: Redis puede manejar una gran cantidad de operaciones por segundo, lo que lo hace ideal para colas de tareas y resultados.

  • Compatibilidad con Pub/Sub y listas: la simplicidad de implementar colas y mecanismos de suscripción facilita la organización de interacciones asincrónicas entre servicios.

  • Comparación con otros agentes de mensajes: a diferencia de RabbitMQ o Kafka, Redis requiere menos configuración y proporciona un rendimiento suficiente para sistemas en tiempo real.


Estibador

El papel de Docker:

  • Aislamiento del entorno: los contenedores Docker permiten ejecutar código en un entorno completamente aislado, lo que aumenta la seguridad de ejecución y reduce el riesgo de conflictos con el sistema principal.

  • Manejabilidad y consistencia: el uso de Docker proporciona el mismo entorno para compilar y ejecutar código, independientemente del sistema host.

  • Comparación: ejecutar código directamente en el host puede representar un riesgo de seguridad y generar conflictos de dependencia, mientras que Docker le permite resolver estos problemas.


WebSocket

  • Tiempo real: la conexión persistente con el cliente permite que los datos (registros, resultados de ejecución) se transfieran instantáneamente.
  • Experiencia de usuario mejorada: con WebSocket, el IDE puede mostrar dinámicamente los resultados del código.


3. Beneficios de la arquitectura de microservicios

Este proyecto utiliza un enfoque de microservicio, que tiene una serie de ventajas importantes:


  • Escalabilidad independiente: cada servicio (servidor gRPC, Worker, servidor WebSocket, Redis) se puede escalar por separado en función de la carga. Esto permite un uso eficiente de los recursos y una rápida adaptación al crecimiento del número de solicitudes.
  • Tolerancia a fallos: dividir el sistema en módulos independientes significa que el fallo de un microservicio no provoca el fallo de todo el sistema. Esto aumenta la estabilidad general y simplifica la recuperación de errores.
  • Flexibilidad de desarrollo e implementación: Los microservicios se desarrollan e implementan de forma independiente, lo que simplifica la introducción de nuevas funcionalidades y actualizaciones. Esto también permite utilizar las tecnologías más adecuadas para cada servicio específico.
  • Facilidad de integración: Las interfaces claramente definidas (por ejemplo, a través de gRPC) facilitan la conexión de nuevos servicios sin realizar cambios importantes en la arquitectura existente.
  • Aislamiento y seguridad: cada microservicio puede ejecutarse en su propio contenedor, lo que minimiza los riesgos asociados con la ejecución de código inseguro y proporciona una capa adicional de protección.


4. Análisis comparativo de planteamientos arquitectónicos

Al crear IDEs WEB modernos para la ejecución remota de código, se suelen comparar distintas soluciones arquitectónicas. Consideremos dos enfoques:


Enfoque A: Arquitectura de microservicios (gRPC + Redis + Docker)


  • Latencia: 40 ms
  • Rendimiento: 90 unidades
  • Seguridad: 85 unidades
  • Escalabilidad: 90 unidades


Características:
Este enfoque proporciona una comunicación rápida y confiable entre servicios, un alto aislamiento de la ejecución del código y una escalabilidad flexible gracias a la contenedorización. Es perfecto para los IDE WEB modernos, donde la capacidad de respuesta y la seguridad son importantes.


Enfoque B: Arquitectura monolítica tradicional (HTTP REST + ejecución centralizada)


  • Latencia: 70 ms
  • Rendimiento: 65 unidades
  • Seguridad: 60 unidades
  • Escalabilidad: 70 unidades


Características:
Las soluciones monolíticas, que se utilizan a menudo en las primeras versiones de los IDE web, se basan en HTTP REST y en la ejecución centralizada de código. Estos sistemas se enfrentan a problemas de escalabilidad, mayor latencia y dificultades para garantizar la seguridad al ejecutar el código de otra persona.


Nota: En el contexto moderno del desarrollo de WEB IDE, el enfoque de ejecución centralizada y HTTP REST es inferior a las ventajas de una arquitectura de microservicios, ya que no proporciona la flexibilidad y escalabilidad necesarias.


Visualización de métricas comparativas

El gráfico muestra claramente que la arquitectura de microservicios (Enfoque A) proporciona menor latencia, mayor rendimiento, mejor seguridad y escalabilidad en comparación con la solución monolítica (Enfoque B).


5. Arquitectura Docker: aislamiento y escalabilidad

Uno de los elementos clave de la seguridad y estabilidad del sistema es el uso de Docker. En nuestra solución, todos los servicios se implementan en contenedores separados, lo que garantiza:


  • Aislamiento del entorno de ejecución: Cada servicio (servidor gRPC, Worker, servidor WebSocket) y broker de mensajes (Redis) se ejecutan en su propio contenedor, lo que minimiza el riesgo de que código inseguro afecte al sistema principal. Al mismo tiempo, el código que el usuario ejecuta en el navegador (por ejemplo, a través del WEB IDE) se crea y ejecuta en un contenedor Docker separado para cada tarea. Este enfoque garantiza que el código potencialmente inseguro o erróneo no pueda afectar el funcionamiento de la infraestructura principal.
  • Consistencia del entorno: el uso de Docker garantiza que las configuraciones permanezcan iguales en los entornos de desarrollo, prueba y producción, lo que simplifica enormemente la depuración y garantiza la previsibilidad de la ejecución del código.
  • Flexibilidad de escalabilidad: cada componente se puede escalar de forma independiente, lo que le permite adaptarse de manera eficaz a las cargas cambiantes. Por ejemplo, a medida que aumenta la cantidad de solicitudes, puede iniciar contenedores Worker adicionales, cada uno de los cuales creará contenedores separados para ejecutar el código del usuario.

En este esquema, Worker no solo recibe tareas de Redis, sino que también crea un contenedor separado (Contenedor: Ejecución de código) para cada solicitud para ejecutar el código del usuario de forma aislada.


6. Pequeñas secciones de código

A continuación se muestra una versión minimizada de las secciones principales del código que demuestra cómo funciona el sistema:

  1. Determina qué idioma se ejecutará utilizando el registro de ejecución global.
  2. Inicia un contenedor Docker para ejecutar el código de usuario mediante la función RunInDockerStreaming.



1. Detección de idioma mediante el registro del corredor

El sistema utiliza un registro global, donde cada idioma tiene su propio ejecutor. Esto permite agregar fácilmente soporte para nuevos idiomas, basta con implementar la interfaz del ejecutor y registrarla:


 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") }


Ejemplo de registro de un nuevo idioma:


 func init() { languages.Register("python", NewGenericRunner("python")) languages.Register("javascript", NewGenericRunner("javascript")) }


Así, al recibir una solicitud, el sistema llama a:


 runner, err := languages.GetRunner(req.Language)


y recibe el corredor correspondiente para ejecutar el código.


2. Lanzar un contenedor Docker para ejecutar código

Para cada solicitud de código de usuario, se crea un contenedor Docker independiente. Esto se hace dentro de los métodos de ejecución (por ejemplo, en Run). La lógica principal para ejecutar el contenedor se encuentra en la función 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") } }


Esta función genera el comando docker run, donde:


  • La imagen es la imagen de Docker seleccionada para un idioma específico (definido por la configuración del ejecutor).
  • dir es el directorio con el código creado para esta solicitud.
  • cmdStr es el comando para compilar o ejecutar el código.


Así, al llamar al método Run del corredor, ocurre lo siguiente:


  • La función RunInDockerStreaming inicia el contenedor Docker donde se ejecuta el código.
  • Los registros de ejecución se transmiten al canal logCh, que permite transmitir información sobre el proceso de ejecución en tiempo real.


3. Proceso de ejecución integrado

Fragmento minimizado de la lógica principal de ejecución del código (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 } }


En este ejemplo mínimo:


  • La detección del idioma se realiza mediante una llamada alanguages.GetRunner(req.Language), que permite agregar fácilmente soporte para un nuevo idioma.
  • El lanzamiento del contenedor Docker se implementa dentro de los métodos Compile/Run, que usan RunInDockerStreaming para ejecutar el código de forma aislada.


Estos fragmentos clave muestran cómo el sistema admite la extensibilidad (fácil incorporación de nuevos idiomas) y proporciona aislamiento mediante la creación de un contenedor Docker independiente para cada solicitud. Este enfoque mejora la seguridad, la estabilidad y la escalabilidad de la plataforma, lo que es especialmente importante para los IDE WEB modernos.

7. Conclusión

En este artículo se analiza una plataforma para la ejecución remota de código basada en una arquitectura de microservicios que utiliza la pila gRPC + Redis + Docker. Este enfoque le permite:


  • Reduzca la latencia y garantice un alto rendimiento gracias a una comunicación eficiente entre servicios.
  • Garantice la seguridad aislando la ejecución del código en contenedores Docker separados, donde se crea un contenedor separado para cada solicitud de usuario.
  • Escalar el sistema de forma flexible gracias al escalado independiente de microservicios.
  • Entregue resultados en tiempo real a través de WebSocket, lo que es especialmente importante para los IDE WEB modernos.


Un análisis comparativo muestra que la arquitectura de microservicios supera significativamente a las soluciones monolíticas tradicionales en todas las métricas clave. Las ventajas de este enfoque se confirman con datos reales, lo que lo convierte en una solución atractiva para crear sistemas de alto rendimiento y tolerantes a fallas.



Autor: Oleksii Bondar
Fecha: 2025–02–07