Temporal Execution Platform 리뷰 #1

오픈소스 Execution 플렛폼인 Temporal에 대해 소개하고 workflow 동작 방삭에 대해 살펴 봅니다.

2023-12-03

소개

서비스 아키텍쳐는 시대에 따라 많은 변화를 겪어 왔습니다. 단순 모놀리딕 코드베이스와 배포 패키지에서 MSA로 변화하였고 API 기반의 동작에서 CQRS와 같은 이벤트 드리븐 방식으로 변화를 해 가는 중 입니다.

제가 구지 발전이라는 단어대신 변화라고 이야기한 이유는 혹자는 이러한 변화를 불편해하거나 흐름에 편승하여 변화를 선택하고 후회하는 경우도 봤기 때문입니다. 우리는 확증편향 못지 않게 소셜 프루프 인식 편향(Social Proof Bias)의 영향아래 살고 있습니다. 아시다 싶이 개발자들은 자신이 보유한 기술이 언제 쓸모없게되지는 않을까 불안해 하거나 남들보다 앞서 defacto 기술을 찾아내어 선점 효과를 누리고자 하는 초조함 속에 살고있습니다. 온라인 커뮤니티나 대형 기업이 주관하는 컨퍼런스에서 전달하는 키워드들은 진정한 가치에 기반하는지 포장만 살짝 바꾼 마케팅 용어에 불가한지 판단하기도 전에 기업과 조직내에서 도그마로 자리잡습니다.

프로그래밍 멘탈 모델의 경우 procedural, object oriented, declarative, event driven, stream reactive 등의 트렌드가 우리를 초조하게 만들었습니다. 그러다 불현듯 다시 functional이라는 procedural한 트렌드로 회귀하는 듯한 보습이 보입니다. 어찌보면, 우리가 처음 적용한 멘탈 모델이 사실은 가장 이해하기 쉽고 직관에 기반한 것이 아날까라는 생각을 하는 중에 놀라운 오픈소스 프로젝트를 발견하게 됩니다.

소프트웨어 아키텍팅에서도 MSA, Event driven과 같이 손에 잡힐듯 말듯한 방법론에서 procedural한 방식으로 회귀하는 모습을 찾을 수 있는 오픈소스가 나왔는데 그것이 Temporal입니다.

Temporal이 제공하는 솔루션이 새로운 만큼 이를 정의하는 적절한 용어를 찾기는 쉽지 않습니다. 저는 temporal이 제공하는 솔루션을 workflow runtime이라 부르기로 했습니다.

당장 실무에 적용하기에는 정치적 기술적 어려움이 따르겠으나 workflow runtime이라는 새로운 솔루션은 충분히 주목할만한 가치가 있다고 생각합니다.

Tech stack

Workflow runtime은 Go 언어로 작성하였습니다.

통신은 protobuf를 사용합니다.

make start-dependencies를 수행하면 필요한 오픈소스를 docker compose를 사용하여 설치합니다. grafana, elasticsearch, cassandra, postgresql, prometheus, mysql 등을 사용합니다.

start dependencies

make install-schema 명령으로 cassandra 스키마를 생성합니다.

executions, tasks, task_queue_user_data, namespaces, queues 등의 테이블을 생성합니다.

make start로 서버를 띄운뒤 http://localhost:8080/로 접근을 하면 다음과 같은 화면이 나옵니다.

Alt text

@temporalio/core-bridge

Rust SDK 바인딩

@temporalio/proto

  • protobuf.js: Temporal 서버와의 통신 프로토콜

@temporal/worker

  • memfs: JavaScript file system utility
  • unionfs: 복수의 file system을 동시에 사용하도록 하는 유틸
  • mozilla/source-map: workflow 코드의 소스맵 생성에 사용
  • RxJS: worker의 이벤트 로직 처리에 사용

Get started with Temporal and TypeScript | Learn Temporal

해당 예제는 default namespace가 존재한다는 전제하에 설명하고 있습니다.

반면, Local에서 직접 실행할 경우 default namespace는 존재하지 않습니다.

참고: What is a Temporal Namespace? | Temporal Documentation

따라서 가이드에 맞춰 temporal CLI로 dev 서버를 다음과 같이 동작 시켜 봅니다.

temporal server start-dev

http://localhost:8233/ 에 접속하면 다음과 같이 Temporal UI를 확인할 수 있습니다.

Alt text

영속성을 위해 다음과 같이 sqlite db를 명시적으로 생성합니다.

temporal server start-dev --db-filename temporal.sqlite

먼저 worker 프로세스를 실행합니다.

npm run worker

다음 명령으로 workflow를 실행합니다.

npm run client

이제 하나의 workflow 실행 이력을 확인할 수 있으며 Alt text

activity 수행 이벤트를 확인할 수 있습니다. Alt text

Worker

Worker는 temporal 서버에 접속해서 workflow와 activity를 수행하는 역할을 합니다. node.js, TypeScript, RxJs 등으로 구현하였고 rust로 작성한 sdk를 바인딩하여 서버와 통신합니다.

다음은 초기화 과정을 보여줍니다.

initial

WorkflowCodeBundler는 workflow를 작성한 코드를 임포트할 수 있는 코드를 동적으로 작성하고 webpack을 사용하여 번들링합니다.

Bundling하는 템플릿은 대략 다음과 같습니다.

const api = require('@temporalio/workflow/lib/worker-interface.js');

api.overrideGlobals();

exports.api = api;

exports.importWorkflows = function importWorkflows() {
  return require(/* webpackMode: "eager" */ `${JSON.stringify(this.workflowsPath)}`);
}

exports.importInterceptors = function importInterceptors() {
  return [
    require(/* webpackMode: "eager" */ `${interceptorImports}`)
  ];
}

분석 당시의 코드는 다음과 같습니다.

const api = require('@temporalio/workflow/lib/worker-interface.js');

api.overrideGlobals();

exports.api = api;

exports.importWorkflows = function importWorkflows() {
  return require(/* webpackMode: "eager" */ "/{your project root}/money-transfer-project-example/lib/workflows.js");
}

exports.importInterceptors = function importInterceptors() {
  return [
    require(/* webpackMode: "eager" */ "/{your project root}/sdk-typescript/packages/worker/lib/workflow-log-interceptor.js")
  ];
}

overrideGlobals은 nodejs의 Date, setTimeout 등 런타임 API를 몽키패칭합니다.

Worker의 주요 함수들은 rxjs 기반으로 구현하였습니다. 다양한 소스로 부터의 이벤트에서 복잡한 로직을 처리하기 위해 채택한 것으로 보입니다.

run 함수역시 rxjs로 구현하였습니다. 다음과 같은 흐름으로 workflow, activity observable 객체를 처리합니다.

stateSubject는 다음과 같은 상태를 가집니다.

workflow는 다음과 같은 흐름으로 처러합니다.

activity는 다음과 같은 흐름으로 처리합니다.

Workflow VM

Worker의 node.js main thread는 서버와 통신하는 Rust 라이브러리 바인딩을 통해 workflow 실행 이벤트를 처리합니다.

Main thread는 Protobuf 형식의 바이너리 정보를 JSON 형식으로 시리얼라이즈 하고 실제 수행은 별도의 node.js web worker로 요청합니다.

initRuntime 함수에서 importInterceptorsimportWorkflows를 차례로 호출하여 worker 생성시 전달한 workflow 디렉터리 경로에 있는 workflow 구현체들을 로드 합니다.

마치며

사용자는 worker와 client로 나누어 코드를 실행해야 합니다.

worker는 workflow 경로에 있는 사용자 구현체를 intercepting할 수 있는 코드를 동적으로 생성하고 webpack으로 번들링 합니다. Temporal 서버와 연결하여 workflow 실행 이벤트를 받아 실제로 수행하고 각 과정(activity)에서 발생한 입력과 결과를 서버에 전달합니다.

client는 workflow를 수행하지만 실제로는 client 프로세스내에서 동작하지 않습니다. workflow 구현체를 호출하면 Temporal 서버에 workflow의 실행을 알리고 각 파라미터를 전달합니다.

이번 포스트에는 workflow의 초기화 위주로 리뷰를 해 보았습니다.

Workflow에 속한 activity는 client에서 invoke 하지만 Temporal 서버의 중계와 로깅을 거쳐 worker에서 수행합니다. 다음 시간에는 activity의 동작 방식에 대해서 다루어 보도록 하겠습니다.

Loading script...