Conversation
Reentrant read/write buffers (fileio_read_backup_to / _from), per-node reorder queue (FILEIO_REORDER_QUEUE) with monotonic single-writer emit, N concurrent pread workers replacing the mutex+io_type serialized path. Byte-identical output to the serial path; restore path unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ated) Adds an optional single-owner async-write double-buffer on top of the existing parallel-read backup (dcabb75). One thread keeps exclusive ownership of all bkup.* + rollover state and overlaps the next chunk's packing against the previous chunk's in-flight aio_write. Steps 1-4 implemented (file_io.c/.h only): - same-disk auto-OFF gate (positive-proof different-device required) + CUBRID_BACKUP_ASYNC_WRITE env opt-in; =force bypasses st_dev for testing - fileio_flush_backup split: dispatcher -> _sync (verbatim, default) vs async - 2-slot buffer_ring; aio issue/reap (<=1 in-flight, submission order), rollover quiesce, teardown final-reap; MAX_VOLUME_SIZE>0 -> sync-positional - SA mode untouched (num_threads=1) Verified on a single-disk box (correctness only; perf needs split-disk): - gate OFF = byte-identical to dcabb75 (code-review APPROVE + restore RT) - async FORCE: restore round-trip t1=2000/t2=100M OK, no cub_server crash - code-review (async): 0 CRITICAL/HIGH Deferred before production (per design + review): - perf GATE-0 on split-disk (this box is device-bound, gain ~0) - MEDIUM-1: restrict async to FILEIO_BACKUP_VOL_DIRECTORY (tape/raw unsafe) - Step 5 fault injection; Step 6 glibc-AIO helper-thread gate (RHEL9); Step 7 completion-boundary quiesce vs log-active archive - aio-across-rollover (currently sync fallback) Design: exp_work/backup_parallel_design.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d path Fixes - fileio_write_backup_volume (): the writer detached each run of consecutive filled slots by pushing onto local_head, so a run of N pages went into the backup stream in descending pageid order. The stream is a seek-free sequential append that restore replays in order, so any run longer than one page produced an image that cannot be restored. Append at the tail instead. - fileio_allocate_node (): the NULL check after the area malloc tested node_p, which is already known non-NULL there, so an area allocation failure returned a node with area == NULL and the reader dereferenced it. Test node_p->area. Preexisting, but this branch enlarges the node pool and widens the window. Dead code - FILEIO_NODE::ready (always equivalent to slots[i] != NULL) and ::writeable - FILEIO_THREAD_INFO::pageid, ::eof_pageid, ::io_type and the FILEIO_TYPE enum - FILEIO_REORDER_QUEUE::dispatch_high_water (always == next_read_pageid) - the callerless fileio_read_backup () wrapper and fileio_append_queue () - the no-op dbfile.area save/restore pairs on the serial path - the unused queue_p parameter of fileio_start_backup_thread () Naming - rq, rq_pool_get and rq_pool_free become reorder_queue, reorder_queue_p, fileio_reorder_queue_pool_get and fileio_reorder_queue_pool_free; src/storage spells "queue" out and carries no _q abbreviation - held_pid becomes held_pageid - next_read_pageid moves from FILEIO_THREAD_INFO into FILEIO_REORDER_QUEUE so that both cursors live in the queue Comments - drop references to the unpublished design note (DESIGN 3.4, CS1, CS3, step2) and the PR-sequencing notes, and describe lock state instead - shorten the capacity, tombstone and node-pool comments - record that treating nread == 0 as fatal predates this work No functional change beyond the two fixes. cubrid, cubridsa and cubridcs all compile warning-free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
❌ TC Merge Gate — Merge BlockedOne or more TC PRs are still open. Please merge or close them before merging this PR. TC Repositories & Branches:
Steps to unblock:
|
🧪 TC Test Environment ReadyCircleCI Testing:
TC Repositories & Branches:
Next Steps:
|
The code-style CI job runs indent on every added or modified file and requires the tree to stay clean afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/run all |
|
다중 볼륨 백업의 메모리 수명 오류와 active-log header 정리 누락이 있으므로 현재 상태로는 병합하기 안전하지 않습니다. Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'upstream/d..." |
|
@InChiJun 리뷰했습니다. 판정: block 설계 방향(pread 동시 읽기 + 순서 복원 큐 + 단일 writer)은 타당하고 출력 바이트 동일성 논거도 납득했습니다. 다만 다중 볼륨에서 reorder 큐 수명이 깨져 debug 빌드 assert 가 확정적으로 실패하고, writer 가 쓰기 오류 이후에도 계속 기록하며, 검증 시나리오와 성능 수치가 이슈에 없습니다. 세 가지 모두 머지 전에 해결이 필요합니다. 그리고 변경 규모가 1,227 LOC 라 한 번에 리뷰하기 어려웠습니다. 아래 D7 과 묶어 분리를 요청드립니다. 주요 결함
D1 — 다중 볼륨에서 reorder 큐 수명이 깨집니다 (blocker)greptile 이 P1 로 잡은 건인데, 엣지 케이스가 아니라 모든 백업에서 발생합니다. fileio_backup_volume 은 볼륨마다 호출되고(log_page_buffer.c 의 데이터 볼륨 루프 + log_info + log_active + archive), fileio_start_backup_thread 가 그 안에서 매번 실행됩니다. 그런데 이 함수는 기존 slots 배열을 해제하지 않고 calloc 합니다. 또 pool_total 을 0으로 리셋하는데, free_list 는 이전 볼륨 노드를 그대로 들고 있습니다. act_r_threads 가 MIN(act_r_threads, from_npages) 라 볼륨마다 capacity 도 달라집니다. 결과적으로 fileio_finalize_backup_thread 의 assert (freed == pool_total) 이 debug/optdebug 빌드에서 확정적으로 실패합니다. freed 는 누적 실제 노드 수, pool_total 은 마지막 볼륨분만 세기 때문입니다. 볼륨당 slots 배열 누수도 함께 발생합니다. D2 — writer 가 쓰기 오류 후에도 계속 씁니다 (blocker)fileio_write_backup_volume 의 배출 루프에서 fileio_write_backup_node 가 실패하면 abort 를 세우는데, break 가 없어 run 의 남은 노드를 전부 계속 기록합니다. 직렬 경로는 같은 자리에서 즉시 goto error 로 빠집니다. break 가 필요해 보입니다. D3 — 검증 시나리오와 성능 결과가 이슈에 없습니다 (blocker)JIRA 본문은 개요 / AS-IS / TO-BE / 구현 1–6 으로 설계는 촘촘한데, 검증에 해당하는 절이 없습니다. 성능 개선 이슈인데 개선 전후 수치가 첨부돼 있지 않고, 어떤 구성에서 무엇을 확인했는지도 적혀 있지 않습니다. 계측하신 흔적은 있습니다. TO-BE 절 끝의 "계측 결과 writer 대기는 ... 리더 1개 구성에서 최대 40%, 리더 수가 늘수록 9~18%" 가 그것입니다. 다만 이건 순서 대기가 병목이 아니라는 설계 근거로 든 수치이지 개선 결과가 아닙니다. 정작 이 이슈의 주장인 "-t 를 올리면 빨라진다" 를 뒷받침하는 수치가 없습니다. 최소한 아래가 이슈에 첨부되면 좋겠습니다.
그리고 변경 파일이 file_io.c 와 file_io.h 둘뿐이라 TC 도 함께 없습니다. 아래 실패 모드가 전부 미검증으로 남습니다.
시나리오가 이슈에 먼저 적히면 QA 리포지터리 쪽 TC 범위도 같이 정해질 것 같습니다. D4 — 인터럽트 체크가 뮤텍스 밖으로 이동했습니다 (major)develop 에서 pgbuf_is_log_check_for_interrupts 호출은 thread_info->mtx 안에 있어 항상 직렬화돼 있었는데, 3단 리팩터링에서 read/compress 와 함께 락 밖 구간으로 나왔습니다. 이 함수가 순수 조회가 아닌 것이 문제입니다. logtb_is_interrupted 를 clear=true 로 호출해 tdes->interrupt 를 소비하고, 그 안에서 tdes->interrupt 에 대한 비동기화 read-modify-write 와 log_Gl.trantable.num_interrupts 감소가 일어납니다. reader 들은 thread_info->tran_index 를 공유하므로, 동시 진입하면 num_interrupts 가 이중 감소해 실제 pending 인터럽트 수와 어긋납니다. 이 전역이 pgbuf_Pool.check_for_interrupts 를 결정하기 때문에 다른 트랜잭션의 인터럽트 누락으로 이어질 수 있습니다. 트리거 pageid 간격이 100 이고 dispatch 창이 4 × act_r_threads 이므로, 두 트리거가 동시에 in-flight 이려면 act_r_threads 가 26 이상, 즉 -t 27 이상에서 열립니다. 이 PR 의 목적이 -t 를 올려 쓰게 만드는 것이라 무시하기 어려운 조건 같습니다. 이 호출은 가볍고 락 밖에 있을 이유가 없으니, held_pageid 확정 직후 1단계(claim, 락 안)로 되돌리는 것이 어떨까요. 체크 주기는 그대로 유지됩니다. 부수적으로, 락 밖 작업 중인 reader 가 abort 를 확인하지 않는 설계(단일 소유권 유지 목적) 때문에 Ctrl-C 이후 reader 당 최대 1페이지의 추가 read+압축이 발생합니다. 유계라 수용 가능하지만 의도된 선택임을 주석이나 PR 본문에 남겨주시면 좋겠습니다. D5 — nread == 0 의 의미가 스레드 수에 따라 다릅니다 (major)reader 는 nread <= 0 을 치명적 오류로, 직렬 루프는 nread == 0 을 정상 end-of-volume 으로 보고 break 합니다. 주석에 인정돼 있는데, 볼륨이 fstat 이후 줄어들면 -t 1 은 성공하고 -t 2 는 실패한다는 뜻입니다. 주석보다는 어느 쪽이 맞는지 결정이 필요한 사항 같습니다. D6 — JIRA 설명이 코드와 불일치합니다 (major)리뷰 중 설계 문서로 JIRA 본문을 읽었는데, 설명된 것 중 셋이 코드에 없습니다.
그리고 메모리 상한 설명이 사실과 다릅니다. 본문은 "동시에 살아있는 버퍼는 capacity 를 넘지 않는다"고 하는데, writer 가 슬롯을 비우고 락을 푼 뒤 파일에 쓰는 동안 노드는 free_list 로 돌아오지 않습니다. 그 사이 reader 가 집어가는 분까지 합치면 피크는 2 × capacity(reader수 × 8) 입니다. 상한 파라미터가 없어 -t 와 페이지 크기에 그대로 비례합니다. 리뷰어가 없는 필드를 찾게 되니 본문 갱신 부탁드립니다. D7 — 백업 코드 분리와 변경 규모 (major)file_io.c 가 12,151줄에서 12,843줄이 됐고, 함수 본문 기준 46% 정도(약 5,650줄)가 이미 backup/restore 기계장치입니다. fileio_create_backup_volume 부터 fileio_clear_backup_info_level 까지 순수 볼륨 I/O 코드와 뒤섞여 분포해 있습니다. 규모보다 더 걸리는 건 이 PR 로 같은 파일 안에 버퍼 풀 수명 규칙이 세 종류가 된다는 점입니다.
D1 이 정확히 그 경계에서 나온 결함입니다. 백업 기능을 별도 파일로 분리하는 방향은 검토 가능할까요? 분리를 요청드리는 또 하나는 async write 입니다. JIRA 제목과 구현 항목 6개는 전부 읽기 병렬화인데 8da037b 가 통째로 쓰기 측 기능이고, 이 둘을 나누면 변경 규모도 리뷰 가능한 수준으로 내려옵니다. async write 쪽은 검증 가능성이 특히 걸립니다. 게이트가 CUBRID_BACKUP_ASYNC_WRITE 환경변수 + st_dev 불일치 + SERVER_MODE 이므로 기본 설정에서 fileio_emit_slot_positional 이하 약 400줄을 CI 가 한 줄도 밟지 않습니다. 하필 그 400줄이 볼륨 롤오버와 errno 분류라 백업에서 가장 위험한 자리입니다. 함수 자체는 sync 경로의 do-loop 를 pwrite 로 충실히 옮긴 것으로 보이고 라인 단위 오류는 찾지 못했지만, 테스트가 닿지 않는 400줄을 머지하는 것과는 별개 문제 같습니다. 추가로 "force" 값이 안전 게이트(다른 장치 확인)를 우회하는데 프로덕션 코드에 남아 있고, 운영자 가시 동작이 시스템 파라미터가 아닌 미문서 환경변수로 제어됩니다. 그 외
권고block. D1 · D2 · D3 해결 전에는 머지하지 않는 것이 좋겠습니다. 가능하면 읽기 병렬화와 async write 를 별도 PR 로 나눠 주시면, 각각 리뷰 가능한 규모가 되고 D3 의 테스트 범위도 명확해집니다. |
…d path Fixes - fileio_read_backup_to (): the cdc archive strip for the active log header still pointed at session->dbfile.area after the destination became an explicit parameter. That buffer holds the backup file header at the time, whose first member is a positive INT64 size, so the LOGPB_HEADER_PAGE_ID guard always returned early and the strip never ran. Every backup, serial included, kept the source database's archive watermark. Merging develop introduced it: both sides changed the function in ways git could merge textually but not semantically. - fileio_start_backup_thread (): pool_total was reset per volume while free_list is session-lifetime, so the teardown assert (freed == pool_total) fired at the end of every successful multi-volume backup wherever asserts are enabled. The default thread count already takes the parallel path, so this needed no special options. - fileio_start_backup_thread (): the per-volume slots array was reallocated without freeing the previous one, and the calloc failure path orphaned it. - fileio_write_backup_volume (): a failed write published the abort but kept emitting the rest of the detached run, re-entering the out-of-space volume rollover once per remaining node and burying the first error. Stop at the first failure, as before. - fileio_read_backup_to (): --sleep-msecs now paces each reader separately because the read runs outside the mutex, so the session read rate grew with the thread count. Carry the whole session's share in every reader's sleep. - fileio_write_backup_volume (): abort_drain could return NO_ERROR when a worker aborted on a read that set no error code, and the caller would have taken a truncated volume for a completed one. Also snapshot errid under the mutex. - fileio_start_backup_thread (): use the writer's return value instead of re-reading the abort flag and both cursors without the mutex. Dead code - fileio_delete_queue_head () and the drain loop that called it: the only size++ was in fileio_append_queue (), removed earlier in this branch, so it could never run - FILEIO_QUEUE::size/head/tail and FILEIO_NODE::prev, write-only once that went - the unused rv and conn_p locals Comments - drop the PW Step markers, which point at an implementation plan that is not in the tree, and the claim that async_enabled is always false - it is opt-in, not constant - record the real node high-water mark: 2 x capacity, not capacity - note in the ZLIB arm that its source is the session header page, not the node cubrid, cubridsa and cubridcs all compile warning-free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
H2SU
left a comment
There was a problem hiding this comment.
코멘트 ① src/storage/file_io.c:8930 (Blocking)
Blocking — async 쓰기 경로가 non-seekable 백업 대상에서 실패합니다 (테이프/named pipe/raw device)
fileio_emit_slot_positional() / fileio_flush_backup_issue() 는 pwrite() 와 aio_write() 로
명시 오프셋에 씁니다. 그런데 async 게이트(L6957~6998)는 opt-in 여부와 st_dev 불일치만 보고,
session_p->bkup.dtype == FILEIO_BACKUP_VOL_DEVICE(L6829 에서 FIFO/캐릭터/블록 특수파일에 설정)
를 배제하지 않습니다.
동기 경로에는 이 전제가 주석으로 명시돼 있습니다 (L9304):
NOTE that we do not call fileio_write since it will try to do lseek and some backup devices do not seek.
async 경로는 이 전제를 그대로 깨뜨립니다.
재현 — FIFO 를 백업 대상으로 지정 (mkfifo /tmp/bkfifo 후 cat 으로 수신):
$ cubrid backupdb -D /tmp/bkfifo -t 4 --no-check td
# CUBRID_BACKUP_ASYNC_WRITE 미설정
rc=0, 파이프로 3,159,040 바이트 정상 통과
# CUBRID_BACKUP_ASYNC_WRITE=1 ← 문서화된 opt-in 값
rc=1, An I/O error occurred while writing page 5 of volume "/tmp/bkfifo".... Illegal seek
78,848 바이트에서 중단
# CUBRID_BACKUP_ASYNC_WRITE=force
rc=1, 동일
Illegal seek 는 pwrite() 의 ESPIPE 이고, 이 errno 는 L8912~8960 의
EINTR/EAGAIN/EDQUOT/EFBIG/EIO/EINVAL/ENXIO/ENOSPC/EPIPE 분류에 없어 default: 로 떨어져
ER_IO_WRITE 로 백업이 죽습니다.
force 만의 문제가 아닙니다. 위 재현에서 FIFO 는 tmpfs, DB 는 별도 파일시스템이라
out_stat.st_dev != db_stat.st_dev(L6979) 가 성립합니다. 즉 "TEST-ONLY" 라고 표기된 force
없이, 문서화된 =1 만으로도 async 가 켜지고 동일하게 실패합니다. 백업 대상을 DB 와 다른
장치에 두는 것은 이 기능이 권장하는 바로 그 구성입니다.
부수적으로, raw partition 을 위한 nbytes == 0 분기(L8933~8936)도 pwrite 에서는
그런 식으로 short-write 가 나지 않으므로 도달 불가가 됩니다.
제안: 게이트에 session_p->bkup.dtype == FILEIO_BACKUP_VOL_DIRECTORY 조건을 추가해
특수 파일 대상에서는 async 를 켜지 않도록 해주세요. (최소한 ESPIPE 를
is_interactive_need_new 로 분류하는 것만으로는 부족합니다 — 해당 대상에서는 positional write
자체가 성립하지 않습니다.)
코멘트 ② src/storage/file_io.c:6957 (Blocking)
Blocking — 프로덕션 기능 게이트가 문서화되지 않은 환경변수이고, 릴리스 빌드에 그대로 실립니다
envvar_get ("BACKUP_ASYNC_WRITE") 에는 #if defined(CUBRID_DEBUG) 같은 컴파일 가드가 없습니다.
릴리스가 추가하는 -DNDEBUG 는 이 블록을 제거하지 않으므로, CUBRID_BACKUP_ASYNC_WRITE 는
고객이 받는 바이너리에서 그대로 동작합니다.
실측 — 평범하게 빌드한 서버에서 strace -c 로 cub_server 의 쓰기 syscall 을 셌습니다
(backupdb -t 16 --no-check, 동일 DB, 동일 바이너리):
env 미설정 : write 1342 회, pwrite64 21 회
CUBRID_BACKUP_ASYNC_WRITE=force : write 93 회, pwrite64 1270 회
서버 프로세스 환경변수 하나로 백업 쓰기 경로 전체가 교체됩니다.
세 가지를 함께 봐주셨으면 합니다.
- CUBRID 관례상 운영자가 보는 knob 은
cubrid.conf의 시스템 파라미터(prm_get_*)입니다.
그래야 문서화되고,cubrid paramdump에 노출되고, DB 별로 설정됩니다. 세션 초기화 시점에
읽는 env 는 어떤 진단에도 잡히지 않고, 서버 프로세스 환경을 쥔 쪽이 임의로 켤 수 있습니다. force는 바로 위 40 줄 주석이 "do no harm 조건" 이라고 정당화한 different-device 검사를
의도적으로 무력화합니다. TEST-ONLY 라고 적혀 있지만 이를 강제하는 장치가 없습니다.- 기본 OFF 라 CI 커버리지가 0 인데, 담고 있는 건 out-of-space / 볼륨 롤오버처럼 백업에서
가장 까다로운 의미론입니다.
제안: (a) 시스템 파라미터로 승격하거나, (b) async 더블버퍼를 이 PR 에서 분리해
별도 PR 로 올려주세요. PR 제목은 "parallelize volume reads" 인데 async writer 는
범위 밖이고, 기본 OFF 로 출하되므로 검증 없이 코드만 남습니다.
코멘트 ③ src/storage/file_io.c:8455 (Non-blocking, 개선 요청)
백업 중 서버 메모리가 스레드 수에 비례해 늘고to` 포함)
capacity = MAX (act_r_threads, 1) * 4 이고, 바로 위 주석대로 live 노드는 최대 2 * capacity
까지 갑니다. 노드당 FILEIO_DBVOLS_IO_PAGE_SIZE(full backup + 16K 페이지 → 512KB)에
압축을 켜면 LZ4 bound 버퍼가 붙어 대략 두 배가
실측 — 80 core / 251GB 호스트, 2.0GB 볼륨(랜덤 데이터 1.3GB), 측정마다 서버를 재시작해
힙을 초기화한 뒤 백업 1 회 동안의 cub_server peak RSS 증가분:
-t 1 +1.9 MB -t 8 +53.1 MB -t 32 +231.2 MB
-t 64 +363.1 MB -t auto +364.4 MB ← auto 가 기본값
압축을 끄면(--no-compress) -t 64 에서 +139 MB 로, 켰을 때의 절반 수준입니다.
주석에 적힌 "memory grows with thread count and page size with no cap" 이 그대로 관측됩니다.
공정을 기하기 위해 덧붙이면, 기존 코드도 스은 조건에서
merge-base 빌드는 -t 64 +195.6 MB, -t auto +255.6 MB 였습니다. 즉 증가는 배수로 1.4~1.9배
수준이지 새로 생긴 성질은 아닙니다. 다만 상한이 없고 기본값이 코어 수를 따라가므로,
코어가 많고 메모리가 빠듯한 서버에서는 backupdb 한 번이 수백 MB 를 추가로 요구합니다.
제안: capacity 에 상한을 두는 시스템 파라미터(또는 최소한 하드 상한)를 넣어주세요.
pool_total 이 세션 종료 전까지 줄지 않는 점도
덧붙여 L8129 의 fileio_reorder_queue_pool_get() 은 thread_info_p->mtx 를 잡은 채
노드 malloc(압축 시 약 1MB)을 수행합니다. 워밍 의 allocator
호출 뒤에 줄을 서게 되는데, 이 PR 이 없애려던 직렬화와 같은 성질입니다.
free_list 에서 꺼내는 것만 락 안에서 하고 새 할당은 락 밖으로 빼는 편이 좋겠습니다.
http://jira.cubrid.org/browse/CBRD-27003
Purpose
backupdb -t N에서 볼륨 페이지를 동시에 읽습니다. reorder 큐가 쓰기 직전에 pageid 순서를복원하므로 백업 스트림은 직렬 백업과 바이트 단위로 동일합니다.
Implementation
진입점은
fileio_backup_volume ()→fileio_start_backup_thread ()입니다.큐를 할당하고 reader N개를 띄운 뒤, 호출 스레드가 그대로 writer가 됩니다.
Reader
fileio_read_backup_volume ()— 페이지당 3단계next_read_pageid발급, 풀에서 노드 획득slots[pageid % capacity]에 저장, writer 깨움work 단계를 락 밖에 둘 수 있는 근거는
pread ()입니다. 공유 file offset이 없어직렬화할 이유가 없습니다.
Writer
fileio_write_backup_volume ()—next_emit_pageid부터 연속으로 채워진슬롯 구간을 락 안에서 떼어낸 뒤, 락을 풀고 스트림에 append합니다. 출력은 순차이며
seek이 없습니다.
Reorder 큐
FILEIO_REORDER_QUEUE— 슬롯reader 수 × 4개 링 버퍼,pageid % capacity로 인덱싱합니다. 발급 창(
next_read_pageid - next_emit_pageid < capacity)이 살아 있는 pageid를 한 바퀴 안으로묶어 인덱스 충돌을 막고, 동시에 backpressure 역할을 합니다. 증분 백업에서 건너뛰는
페이지는
tombstone노드로 발행해 writer가 그 pageid를 지나가게 합니다.노드는 단독 소유입니다. reader는 발행하거나 풀에 반납하거나 둘 중 하나만 하고,
오류가 나면
abort를 세운 뒤 두 condvar에 broadcast해 대기 중인 스레드를 모두 깨웁니다.Remarks
fileio_read_backup_to (),fileio_write_backup_from ()). reader가session->dbfile.area를 교체하지 않습니다.CUBRID_BACKUP_ASYNC_WRITE가 설정되고동시에
-D대상이 DB와 다른 장치임이 확인된 경우에만 켜지므로, 기본 동작은변하지 않습니다.
-t 1과 SA 모드는 기존 직렬 경로를 그대로 사용합니다.