API 자동화를 통한 DolphinScheduler 메이저 버전 업그레이드: 3.1.3에서 3.4.1까지
요약
DolphinScheduler 3.1.3에서 3.4.1로의 대규모 업그레이드를 위해 공식 경로 대신 '재구축 및 API 활용' 전략을 채택한 사례를 다룹니다. 기존 환경을 유지한 채 새로운 클러스터를 구축하고, 커스텀 스크립트로 워크플로우를 API를 통해 일괄 마이그레이션함으로써 다운타임을 최소화하고 리스크를 관리했습니다.
핵심 포인트
- 공식 업그레이드 경로(3.1.3 → 3.2.0 → 3.3.0 → 3.4.1)의 복잡성과 누적 다운타임 문제를 해결하기 위해 새로운 환경 구축 전략을 선택함
- 기존 클러스터를 유지하며 새 클러스터로 점진적 전환하는 방식을 통해 비즈니스 연속성을 보장하고 롤백 용이성을 확보함
- 수천 개의 워크플로우를 수동으로 재구축하는 대신, 이전 버전 API에서 데이터를 추출하여 새 버전 API로 일괄 생성하는 자동화 스크립트를 개발함
- 데이터베이스 스키마 변경 리스크를 피하기 위해 과거 실행 기록은 제외하고 핵심 워크플로우 정의 데이터만 마이그레이션함
- 배경: 왜 메이저 버전 업그레이드를 수행하는가?
기존 환경
- 현재 DolphinScheduler 버전: 3.1.3
- 현재 SeaTunnel 버전: 2.1.3
- 배포 규모: 1 Master + 2 Workers, 3,700개 이상의 워크플로우 (workflow) 정의 및 매일 20,000개 이상의 스케줄링된 작업 (scheduled tasks) 실행
- 운영 기간: 시스템이 3년 이상 안정적으로 운영됨
업그레이드 동기
- 기능적 요구사항: 비즈니스 요구사항이 증가함에 따라 아키텍처 설계 제약, 메타데이터 데이터베이스 (metadata database) 처리 병목 현상, 서버 리소스 부족 등 현재 버전의 한계가 점점 더 명확해짐
- 커뮤니티 지원: 더 나은 기술 지원을 받기 위해 최신 안정화 버전을 채택할 것을 공식 커뮤니티에서 권장함
- 성능 최적화: 버전 3.4.1은 스케줄링 성능과 시스템 안정성 면에서 상당한 개선을 제공함
공식 업그레이드 경로를 사용하지 않은 이유
- 큰 버전 차이: 업그레이드 경로를 따를 경우 여러 단계의 중간 업그레이드가 필요함: 3.1.3 → 3.2.0 → 3.3.0 → 3.4.1
- 운영 환경 제약: 엄격한 비즈니스 연속성 요구사항으로 인해 여러 번의 유지보수 시간 (maintenance windows)을 갖는 것은 불가능했음
- 아키텍처 변경 리스크: 리소스 센터 (resource center) 리팩토링, 메타데이터 스키마 (metadata schema) 변경 및 호환성 문제 등의 리스크가 포함됨
- 작업 부하 고려: 수천 개의 워크플로우가 있으므로 작업을 수동으로 재구축하는 것은 엄청난 노력이 필요하며, 따라서 자동화가 필수적이었음
- 전체 마이그레이션 전략: "재구축 + API" 접근 방식을 통한 공식 업그레이드 경로 우회
2.1 핵심 아이디어
"인플레이스 증분 업그레이드 (in-place incremental upgrade)"를 추구하는 대신, 우리는 "새 환경 + 데이터 마이그레이션 (data migration)" 전략을 채택함:
- 기존 3.1.3 클러스터는 운영 워크로드에 영향을 주지 않고 계속 실행됨
- 깨끗한 아키텍처를 보장하기 위해 완전히 새로운 3.4.1 클러스터를 배포함
- 이전 버전의 API로부터 워크플로우 정의 및 작업 구성을 가져오기 위해 커스텀 스크립트를 개발함
- 새 버전의 API를 사용하여 새 클러스터에 워크플로우를 일괄 생성함
비즈니스 스케줄링 트래픽을 새로운 환경으로 점진적으로 전환함.
2.2 장점 및 리스크 비교
| 비교 차원 | 공식 업그레이드 방식 (Official Upgrade Approach) | 재구축 + API 방식 (Rebuild + API Approach) |
|---|---|---|
| 다운타임 (Downtime) | 누적 다운타임이 수 시간 또는 수 일까지 지속될 수 있는 여러 번의 업그레이드 필요 | 기존 스케줄을 중단하고 새 스케줄을 활성화함으로써 거의 끊김 없는 전환 (Seamless cutover) 가능 |
| 롤백 난이도 (Rollback Difficulty) | 데이터베이스 스키마 (Database schema)가 이미 변경되었으므로 어려움 | 기존 환경이 온전하게 유지되므로 쉬움 |
| 데이터 일관성 (Data Consistency) | 모든 스키마 마이그레이션 (Schema migrations)에 대한 검증 필요 | 핵심 비즈니스 데이터(워크플로우 정의)만 마이그레이션되며, 과거 실행 기록은 제외됨 |
| 버전 호환성 (Version Compatibility) | 모든 중간 버전 간의 호환성 문제를 처리해야 함 | 필요한 파라미터 변환만으로 3.4.1 버전에 직접 적응함 |
| 작업 부하 (Workload) | 반복적인 검증 사이클이 필요함 | 노력의 대부분이 스크립트 개발에 집중됨 |
| 적합한 시나리오 (Suitable Scenarios) | 마이너 버전 업그레이드 (Minor version upgrades) | 메이저 버전 점프 (Major version jumps) 및 대규모 작업 마이그레이션 |
- 상세 구현 단계
3.1 환경 준비
3.1.1 새로운 환경 배포
- 완전히 새로운 DolphinScheduler 3.4.1 클러스터를 배포함.
- 데이터베이스 및 레지스트리 (Registry)와 같은 종속성을 구성함. ZooKeeper는 사용 중단(Deprecated)되었으며 JDBC Registry로 대체됨.
- DataX 및 SeaTunnel과 같은 필수 구성 요소를 구성함.
- 다음과 같은 새 버전의 주요 변경 사항을 파악함:
- SeaTunnel 2.1.3 통합: 시작 모드가 Spark 엔진 실행에서 start-seatunnel-spark.sh로 변경됨;
- 테넌트 (Tenants), 워커 그룹 (Worker groups), 환경(Environments)과 같은 기본 설정이 이제 프로젝트 기본 설정(Project preferences)을 통해 관리됨;
- 파라미터 전달 동작 변경: 다운스트림 작업(Downstream tasks)이 상위 작업(Upstream)의 변수 값을 받으려면 IN 파라미터를 명시적으로 정의해야 함.
- 새 환경의 기본 기능을 확인하고 대표적인 워크플로우를 수동으로 검증함.
3.1.2 API 액세스 구성
- 토큰 관리에서 새 토큰을 생성하여 새 환경의 API 액세스 권한을 구성함.
- API 호출을 위한 관리자 토큰을 획득함.
- API 연결성을 확인함.
3.2 메타데이터 데이터베이스 초기화 및 기본 테이블 복제: 이전 버전의 설정에 따라 새로운 메타데이터 데이터베이스에 기초 메타데이터 테이블을 재구축하며, 가능한 경우 ID를 보존합니다. 이를 통해 스크립트 기반의 워크플로 (Workflow) 복원 과정에서 수정 복잡성을 크게 줄였습니다.
검증 항목:
- 테이블 (Table)
- 테넌트 (Tenant) 테이블: t_ds_tenant
- 프로젝트 (Project) 테이블: t_ds_project
- 사용자 (User) 테이블: t_ds_user
- 환경 (Environment) 테이블: t_ds_environment, t_ds_environment_worker_group_relation
- 워커 그룹 (Worker Group) 테이블: t_ds_worker_group
- 데이터 소스 (Data Source) 테이블: t_ds_datasource
3.3 마이그레이션 스크립트 개발
3.3.1 사전 준비 및 테스트
워크플로를 템플릿 기반 태스크 (Template-based task)와 비템플릿 기반 태스크 (Non-template-based task)로 분류합니다. 대표적인 워크플로를 선정하여 새로운 환경에서 실행함으로써 성공적인 실행 여부와 데이터 동기화의 정확성을 검증합니다.
3.3.2 코드 개발 — 기존 워크플로 정의 읽기
... // 모든 워크플로를 가져와 반복적으로 처리함
String processDefinitionUrl = OLD_URL + "/dolphinscheduler/projects/" + oldProjectCode + "/process-definition/query-process-definition-list" ;
Map < String , String > map = new HashMap <>();
map . put ( "projectCode" , oldProjectCode );
String pdRes = httpClientUtilOld . doGetRequest ( processDefinitionUrl , map );
ArrayList < JSONObject > dataList = parseResDataToList ( pdRes );
for ( JSONObject job : dataList ) {
String oldWFCode = job . get ( "code" ). toString ();
Map < String , String > mapPara = new HashMap <>();
String oldurl = OLD_URL + "/dolphinscheduler/projects/" + oldProjectCode + "/process-definition/" + oldWFCode ;
mapPara . put ( "code" , oldWFCode );
mapPara . put ( "projectCode" , oldProjectCode );
String res = httpClientUtilOld . doGetRequest ( oldurl , mapPara );
JSONObject jsonObject = JSON . parseObject ( res );
JSONObject data = ( JSONObject ) jsonObject . get ( "data" );
JSONObject processDefinition = data . getJSONObject ( "processDefinition" );
JSONArray processTaskRelationList = data .
getJSONArray ( "processTaskRelationList" ); JSONArray taskDefinitionList = data . getJSONArray ( "taskDefinitionList" ); // TODO: 새로운 태스크 코드를 생성하고 기존 코드를 교체하세요 // 워크플로 (workflow) 정보를 채우고 워크플로를 생성합니다
createWF ( processDefinition , processTaskRelationList , taskDefinitionList , NEW_IP , newProjectCode );
3.3.3 API를 통한 워크플로 (Workflows) 생성
// 태스크 개수를 기반으로 태스크 코드 생성
int taskCnt = taskDefinitionList . size ();
List < String > taskCodeList = taskDefinitionList . stream () . map ( obj -> ( JSONObject ) obj ) . map ( obj -> obj . getString ( "code" )) . collect ( Collectors . toList ());
try {
// TODO: 태스크 코드 생성
String taskCodeUrl = NEW_URL + "/dolphinscheduler/projects/" + newProjectCode + "/task-definition/gen-task-codes" ;
HashMap < String , String > taskCodeMap = new HashMap <>();
// n개의 태스크 코드 생성
taskCodeMap . put ( "genNum" , String . valueOf ( taskCnt ));
String codeData = httpClientUtilNew . doGetRequest ( taskCodeUrl , taskCodeMap );
Object codes = JSON . parseObject ( codeData ). get ( "data" );
JSONArray taskCodeArr = JSON . parseArray ( codes . toString ());
// 실제 태스크 요구 사항에 따라 다운스트림 (downstream) 입력 파라미터 추가
for ( int i = 0 ; i < taskDefinitionList . size (); i ++) {
JSONObject logTask = ( JSONObject ) taskDefinitionList . get ( i );
if ( “ Condition Logic ” )) {
JSONObject taskParams = logTask . getJSONObject ( "taskParams" );
JSONArray localParams = taskParams . getJSONArray ( "localParams" );
JSONObject hiveParam = new JSONObject ();
hiveParam . put ( "prop" , "hiveAmount" );
hiveParam . put ( "direct" , "IN" );
hiveParam . put ( "type" , "VARCHAR" );
hiveParam . put ( "value" , "" );
localParams . add ( hiveParam );
logTask . put ( "taskParamList" , localParams );
JSONObject paramMap = new JSONObject ();
for ( Object obj : localParams ) {
JSONObject param = ( JSONObject ) obj ;
paramMap . put ( param . getString ( "prop" ), param . getString ( "value" ));
}
logTask .
put ( "taskParamMap" , paramMap ); .... // 필요한 파라미터(예: 태스크 코드 및 SeaTunnel 실행 엔진)를 교체합니다. for ( int i = 0 ; i < taskCodeList . size (); i ++) { String oldCode = taskCodeList . get ( i ); String newCode = taskCodeArr . getString ( i ); // 태스크 코드를 교체합니다. // SeaTunnel 엔진을 교체합니다: "SPARK" -> "start-seatunnel-spark.sh" taskDefinitionListJsonStr = taskDefinitionListJsonStr . replace ( "code":
스케줄을 활성화하지 않고 워크플로우 배포: 스케줄을 활성화하기 전에 워크플로우를 먼저 배포합니다. 수동 검증: 워크플로우를 배치 단위로 수동 실행하여 기존 클러스터와의 충돌 여부를 확인합니다. 대부분의 워크플로우가 매일, 매시간 또는 15분마다 실행되었기 때문에 충돌은 최소화되었습니다. 실패한 태스크 조사: 근본 원인을 분석하고, 문제를 수정하며, 실패한 워크플로우를 재실행합니다. 스케줄링 설정 활성화: 모든 워크플로우가 검증을 통과하면 스케줄을 활성화합니다. 기존 클러스터 스케줄링 비활성화: 새 환경에서 안정적인 운영을 확인한 후, 기존 클러스터의 해당 스케줄을 비활성화합니다. 프로젝트별, 배치별 마이그레이션
3.4.2 마이그레이션 실행 결과
이 프로세스에 따라, 먼저 199개의 워크플로우를 포함하는 대표 프로젝트를 선정했습니다. 마이그레이션 후, 일주일 동안 운영 환경에서 테스트를 진행했으나 문제가 없었습니다. 이후 약 10일 이내에 총 3,700여 개의 워크플로우를 포함하는 50개 프로젝트의 마이그레이션을 완료했습니다.
추적 테이블
| 번호 | 프로젝트 이름 | 프로젝트 코드 | 워크플로우 수 | 진행 상황 | 비고 |
|---|---|---|---|---|---|
| 1 | Project 1 | 13******* | 199 | 04/16 완료 | 1주일 안정성 테스트 완료 |
| 2 | Project 2 | ... | ... | ... | ... |
| ... | ... | ... | ... | ... | ... |
| 50 | ... | ... | ... | ... | ... |
3.4.3 런타임 상태
새 클러스터는 현재 거의 한 달 동안 가동되었습니다. 이전에는 스케줄링 지연이 10초에서 심한 경우 1분 이상 발생하기도 했습니다. 업그레이드 이후, 스케줄링 지연(latency)이 사실상 제거되었습니다. 스케줄링 인스턴스 누락과 관련된 문제 또한 재발하지 않았습니다. 현재까지 시스템은 식별된 문제 없이 원활하게 운영되고 있으며, 지속적인 모니터링을 유지하고 있습니다.
- 리스크 관리 및 비상 계획
4.1 주요 리스크
데이터 손실 위험: 마이그레이션 과정에서 일부 설정이 누락될 가능성이 있습니다.
호환성 문제: 이전 버전에서 지원되던 특정 설정이 새 버전에서 지원되지 않을 수 있습니다.
비즈니스 중단 위험: 전환(switchover) 과정에서 스케줄링 지연이 발생할 수 있습니다.
4.2 비상 계획 (Contingency Plans)
4.2.1 롤백 전략 (Rollback Strategy)
새로운 환경에서의 스케줄링을 즉시 중단합니다. 기존 환경의 스케줄링 서비스를 복구합니다. 근본 원인을 분석하고 문제 해결 후 재시도합니다.
4.2.2 데이터 백업 (Data Backup)
기존 환경 데이터베이스의 전체 백업을 수행합니다. 새로운 환경의 초기 설정값을 백업합니다.
- 결론 (Conclusion)
5.1 프로젝트 성과 (Project Outcomes)
비즈니스 중단 없이 교차 버전 업그레이드를 성공적으로 완료했습니다. 자동화된 마이그레이션 스크립트(migration scripts)를 통해 효율성을 크게 향상시키고 수동 오류를 줄였습니다. 새 버전은
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기