Dragonflydb Install & Performance Test


Install(build)

📦 Step 1 - install dependencies

다음과 같은 패키지를 미리 설치해야 합니다.
On Fedora(RHEL,CentOS,Rocky): 📍
$ sudo dnf install -y automake boost-devel gcc git cmake libtool openssl-devel libunwind-devel autoconf-archive patch bison libstdc++

Rocky Linux 9.5: 필요 패키지 설치 및 확인

  1. sudo dnf info automake -> 1.16.2
  2. sudo dnf info boost-devel -> 1.75.0
  3. sudo dnf info gcc -> 11.5.0
  4. sudo dnf info git -> 2.47.3
  5. sudo dnf info cmake -> 3.26.5
  6. sudo dnf info libtool -> 2.4.6
  7. sudo dnf info openssl-devel -> 3.2.2
  8. sudo dnf info libunwind-devel -> 1.6.2
  9. sudo dnf info autoconf-archive -> 2019.01.06
  10. sudo dnf info patch -> 2.7.6
  11. sudo dnf info bison -> 3.7.4
  12. sudo dnf info libstdc++ -> 11.5.0
  13. sudo dnf install -y libstdc++-static -> Error: Unable to find a match
    libstdc++.a 이 파일이 있어야 하는데, Rocky Linux 9.5에는 없음.
    /usr/lib64/libstdc++.so.6 -> so 파일은 있으나 사용하지 않음
    ✅ Rocky Linux 9.5: gcc를 다운받아 컴파일하면 "libstdc++.a" 파일이 만들어짐, "/usr/lib64"에 복사했음.
    자세한 설치 방법은 아래 "◎ GCC 소스 코드 다운로드 및 빌드: libstdc++.a" 참조
  14. sudo dnf info ninja-build -> Error: No matching Packages to list
    ninja를 다운받아서 컴파일해서 설치했음. -> 1.14.0.git
    설치 방법: "Helio > SETUP" ninja-build 참조

◎ GCC 소스 코드 다운로드 및 빌드: libstdc++.a

  1. 다운로드
    [~]$ gcc --version
        gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5)
    • 받을 파일 확인: https://ftp.gnu.org/gnu/gcc/gcc-11.5.0/gcc-11.5.0.tar.gz

    [~]$ wget https://ftp.gnu.org/gnu/gcc/gcc-11.5.0/gcc-11.5.0.tar.gz -> 받기(138M)
    [~]$ tar -xzvf gcc-11.5.0.tar.gz -> 압축 풀기(925M)
    [~]$ cd gcc-11.5.0
  2. 빌드 준비
    [gcc-11.5.0]$ ./contrib/download_prerequisites -> All prerequisites downloaded successfully.
    [gcc-11.5.0]$ mkdir build && cd build
    [build]$ ../configure --enable-languages=c,c++ --disable-multilib
    • "--disable-multilib" 이 옵션을 생략하면 32bit lib 파일을 만들려다 에러가 발생함. 64bit lib만 만들면 되므로 이 옵션이 필요함.
  3. 빌드 및 설치(복사)
    • 필요 패키지 설치
        $ sudo dnf install bzip2-devel -> 1.0.8-10.el9_5
        $ sudo dnf install gmp-devel -> 1:6.2.0-13.el9
        $ sudo dnf install mpfr-devel -> 4.1.0-7.el9
        $ sudo dnf install libmpc-devel -> 1.2.1-4.el9
    [build]$ make -j6 -> 약 5분 이내로 끝남. "-j" 옵션을 사용하지 않으면 상당히 오래 걸림.
        make[1]: Leaving directory '/home/redis/zdis/gcc-11.5.0/build' (로그 마지막 라인)
    [build]$ find . -name "libstdc++.a"
        ./x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.a (size: 43517024)
    [build]$ sudo cp ./x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.a   /usr/lib64/ -> 복사

📦 Step 2 - clone the project

[~]$ git clone --recursive https://github.com/dragonflydb/dragonfly && cd dragonfly
    Cloning into 'dragonfly'...

📦 Step 3 - configure & build

# Configure the build
    [dragonfly]$ ./helio/blaze.sh -release
    [dragonfly]$ ./helio/blaze.sh -release -DCMAKE_CXX_FLAGS="-flto" -> 🚩 이 옵션을 사용하면 성능이 좋아진다고 나와있지만, 테스트 결과 비슷함.

# Build
    [dragonfly]$ cd build-opt
    [build-opt]$ ninja dragonfly
        [633/633] Linking CXX executable dragonfly
        -> "__builtin_memcmp" 관련 warning이 발생했으나 빌드는 성공임.
        -> 자세한 내용은: GPT

📦 Step 4 - 실행

# Run
    [build-opt]$ ./dragonfly --alsologtostderr
    657604 init.cc:127] ./dragonfly running in opt mode.
    657605 uring_proactor.cc:290] IORing with 1024 entries, allocated 98368 bytes, cq_entries is 2048
    657604 proactor_pool.cc:149] Running 8 io threads
    657604 server_family.cc:1092] Host OS: Linux 5.14.0-503.35.1.el9_5.x86_64 x86_64 with 8 threads
    657606 listener_interface.cc:102] sock[21] AcceptServer - listening on 0.0.0.0:6379


Performance Test

📦 dragonfly 실행

[build-opt]$ ./dragonfly --alsologtostderr --proactor_threads 8

• CPU: 실행만 해도 wa가 100%임. CPU 사용률 체크가 어려울 수 있음. wa는 빼고 사용률을 계산해야 함.

Server

  • CPU: Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz 4c/8t
  • Memory: 16GB
  • OS: Rocky Linux 9.5

📦 benchmark 1개 실행

$ ./df_bench_01.sh
  SET: 91166.02 requests per second, p50=0.447 msec
  SUM: 91,166

• CPU: 211.1(us+sy)+98.2(hi+si)=309.3% (39%)

📦 benchmark 2개 실행

$ ./df_bench_02.sh
  SET: 84238.90 requests per second, p50=0.479 msec
  SET: 80853.81 requests per second, p50=0.487 msec
  SUM: 165,091

• CPU: 317.4(us+sy)+149.4(hi+si)=466.8% (58%)

📦 benchmark 4개 실행

$ ./df_bench_04.sh
  SET: 64308.68 requests per second, p50=0.591 msec
  SET: 61360.98 requests per second, p50=0.599 msec
  SET: 61353.46 requests per second, p50=0.607 msec
  SET: 61113.48 requests per second, p50=0.599 msec
  SUM: 248,134

• CPU: 360.7(us+sy)+176.1(hi+si)=536.8% (67%)

📦 benchmark 8개 실행

$ ./df_bench_08.sh
  SET: 45612.11 requests per second, p50=0.863 msec
  SET: 45349.42 requests per second, p50=0.871 msec
  SET: 45169.16 requests per second, p50=0.871 msec
  SET: 44776.79 requests per second, p50=0.863 msec
  SET: 44702.73 requests per second, p50=0.879 msec
  SET: 44605.02 requests per second, p50=0.871 msec
  SET: 44563.28 requests per second, p50=0.879 msec
  SET: 44466.18 requests per second, p50=0.879 msec
  SUM: 359,242

• CPU: 435.6(us+sy)+204(hi+si)=639.6% (80%)

📦 benchmark 16개 실행

$ ./df_bench_16.sh
  SET: 25171.16 requests per second, p50=1.535 msec
  ... 생략 ...
  SET: 24219.53 requests per second, p50=1.655 msec
  SUM: 394,357

• CPU: 448.9(us+sy)+183.8(hi+si)=632.7% (79%)

📦 benchmark 32개 실행

$ ./df_bench_32.sh
  SET: 12338.37 requests per second, p50=2.559 msec
  ... 생략 ...
  SET: 11917.39 requests per second, p50=2.607 msec
  SUM: 385,209

• CPU: 469.3(us+sy)+205.8(hi+si)=675.1% (84%)

📦 benchmark 테스트 요약

💻 8 스레드 테스트 요약

구분bench_01 bench_02 bench_04 bench_08 bench_16 bench_32
Threads 8
1차
91,166 165,091 248,134 359,242 394,357 385,209
증감율 81% 50% 45% 10% -2%
Threads 8
2차
95,538 177,493 258,431 342,880 389,731 386,091
증감율 86% 46% 33% 14% -1%

💻 스레드별 테스트

구분Threads 1 Threads 2 Threads 4 Threads 8 비고
QPS 133,430 162,389 283,255 394,357
증감율 22% 74% 39% 평균 45%


💻 helio 테스트 (참조)

구분bench_01 bench_02 bench_04 bench_08 bench_16
Threads 1 95,739 199,232 185,961 196,860 201,713
Threads 2 104,047 225,908 265,440 245,428 268,836
Threads 4 97,418 174,542 263,917 382,961 412,549
Threads 8 99,472 171,443 277,218 395,362 431,702

🌐 helio 자세한 테스트 내용은 여기를 참조하세요.     (dragonfly는 helio를 바탕으로 만들어졌습니다)


참고 자료

📦 dragonfly/helio에서 CPU wa(I/O wait)이 높은 이유

helio(https://github.com/romange/helio)와 helio를 바탕으로 만든 dragonflydb(https://github.com/dragonflydb)는 서버(S/W)를 실행하면(부하없는 상태) 아래 top CPU core별 자료와 같이 wa(I/O wait)가 100%를 사용합니다.
이유는 무엇인가요?

https://chatgpt.com/s/t_690bf7de3eb08191bde0de2ea16d1638

'helio'나 'dragonflydb' 실행 시 부하가 없는데도 모든 CPU 코어의 wa(disk I/O wait)가 100%에 가까운 현상은 꽤 특이하지만, 구조적으로 예상 가능한 상황입니다. 아래에 그 이유를 기술적으로 상세히 설명드리겠습니다.

🔍 요약 결론
'helio' 및 이를 기반으로 하는 'dragonflydb'는 서버 시작 시 내부 스레드들이 비동기 이벤트 루프(reactor)를 돌리며, 이 루프가 비활성 상태(idle)일 때 'epoll_wait' 또는 'io_uring_enter' 같은 커널 블록 호출을 대기합니다.

리눅스의 'top'은 이런 “I/O 대기(blocked on I/O)” 상태를 wa (I/O wait) 로 잡아내기 때문에, 실제로 CPU는 놀고 있어도 'wa=100%'로 표시됩니다.

즉, CPU가 일을 하는 것이 아니라, “스레드들이 I/O 이벤트를 기다리는 중”이므로 CPU가 완전히 놀고 있음에도 'wa'로 잡히는 것입니다.

🧠 기술적 배경 설명
1. DragonflyDB의 스레드 모델
DragonflyDB는 helio 프레임워크 위에서 동작하며, 'epoll' 기반(또는 'io_uring' 기반)의 event-driven reactor model을 사용합니다.
* 실행 시 여러 개의 스레드('N'개, 기본은 CPU 개수에 맞게)가 생성됩니다.
* 각 스레드는 자기 전용 이벤트 루프를 돌립니다.
* 루프는 다음과 같이 동작합니다:

이때, 아무 이벤트(클라이언트 연결, 타이머, I/O 등) 가 없으면 'epoll_wait'이 커널 호출에서 블록(block) 됩니다.

2. CPU 사용률 통계 ('wa'의 의미)
'top' 명령의 '%wa'는 CPU가 "I/O 요청(디스크, 네트워크, 파일 등)을 기다리는 동안 놀고 있는 비율"을 나타냅니다.   즉, 프로세스가 커널 안에서 I/O 완료를 기다리고 있으면 그 코어는 'idle'이 아닌 'iowait' 상태로 분류됩니다.   따라서 helio나 dragonflydb의 스레드가 'epoll_wait()' 안에서 멈춰 있다면, 리눅스 커널은 “이 스레드는 I/O를 기다리고 있다”고 판단하고, 결과적으로 해당 CPU 코어의 '%wa'가 높게 표시됩니다.

3. 실제 CPU 상태 확인 방법
'top'만 보면 CPU가 100% I/O wait인 것처럼 보이지만, 실제로는 CPU가 놀고 있습니다. 이를 확인하려면 다음 명령을 사용해보세요:
pidstat -p $(pgrep dragonfly) 1 ➡ 아래 자세한 설명 있음.
또는
iostat -xz 1 ➡ 아래 자세한 설명 있음.
* 'pidstat'에서는 실제 CPU time이 거의 0에 가까울 것입니다.
* 'iostat'에서는 디스크 I/O 요청이 거의 없음을 확인할 수 있습니다.
즉, 디스크가 실제로 바쁜 게 아니라, 단지 커널이 “대기 중”일 뿐입니다.

4. 이 동작이 정상적인 이유
DragonflyDB(helio)는 고성능 서버 프레임워크로 설계되어, 시작 시 이미 모든 이벤트 루프 스레드를 생성해둡니다. 이는 “즉시 반응할 준비가 된 상태”를 의미하며, 이 루프가 놀고 있을 때는 커널 블록 상태로 진입합니다.   따라서, 부하가 전혀 없을 때
* CPU 사용률: 0%
* I/O wait: 100%
이 되는 것은 정상이며, 실제 성능 문제를 의미하지 않습니다.

5. 참고 (직접 코드에서 확인 가능)
📋• /helio/util/fibers/uring_proactor.cc:
    UringProactor::MainLoop() → wait_for_cqe() → io_uring_wait_cqes()

📋• /helio/util/fibers/epoll_proactor.cc:
    EpollProactor::MainLoop() → EpollWait() → epoll_wait()

이 호출이 블록되면 스레드는 커널에서 “I/O 대기” 상태로 표시됩니다.

✅ 정리

  • CPU 'wa' 100%: 스레드가 epoll/io_uring에서 대기 중
  • CPU 실제 부하: 거의 0%
  • 실제 문제 여부: 정상 현상 (성능 문제 ❌)
  • 확인 방법: 'pidstat', 'iostat', 'perf top' 등으로 실사용 CPU 확인
  • 원인: DragonflyDB/Helio의 reactor 모델이 idle 시 커널 블록 상태 진입

📚 pidstat, iostat

pidstat와 iostat는 독립된 패키지가 아니고 sysstat 패키지 안에 포함되어 있다.
sudo dnf install -y sysstat -> 12.5.4-9.el9

📚 pidstat

🔍 pidstat 항목 설명

  • %usr | 사용자 모드(user mode) CPU 사용률 | 유저 공간에서 실행된 시간 (ex: 앱 코드 실행)
  • %system | 커널 모드(system mode) CPU 사용률 | 커널 코드 실행 시간 (ex: 시스템 콜)
  • %guest | 가상 머신 게스트(guest) 실행 시간 | 가상화 환경(KVM, QEMU, Xen 등) 에서만 나타납니다.
  • %wait | I/O wait 비율 | 프로세스가 I/O(디스크, 네트워크 등) 완료를 기다리며 잠든 시간
  • %CPU | 총 CPU 사용률 | 해당 PID가 실제로 CPU를 사용한 전체 비율 (%CPU = %usr + %system + %guest)
  • CPU | CPU 코어 번호 | 해당 프로세스가 최근 실행된 물리적 코어 번호

📚 iostat

🔍 iostat 주요 항목 중심으로 해석하는 법

  • 'rkB/s', 'wkB/s' 높음 ➡ 실제 데이터 처리량이 많음
  • 'r/s', 'w/s'는 높지만 'rkB/s', 'wkB/s' 낮음 ➡ I/O 요청이 많지만 각각의 요청 크기가 작음 (랜덤 I/O 패턴)
  • 'rrqm/s', 'wrqm/s' 높음 ➡ 커널이 요청을 효율적으로 병합 중 (좋은 신호)
  • 'r_await' 또는 'w_await' 높음 ➡ 디스크 응답이 느림 (HDD, 병목 가능)
  • 'aqu-sz' 지속적으로 높음 ➡ I/O 큐에 요청이 쌓이고 있음 → 병목
  • '%util' 100% 근처 ➡ 해당 디스크가 항상 바쁨 (CPU idle이라도 디스크는 포화 상태)

📊 'iostat -xz' 출력 항목 설명

  • 항목 | 의미 | 단위 / 범위 | 해석 포인트
  • Device | 측정 대상 장치 이름 | 예: 'sda', 'nvme0n1', 'dm-0' 등 | 디스크 또는 논리 볼륨 이름
  • r/s | 초당 읽기 요청 수 (reads per second) | req/s | 초당 몇 번의 read 요청이 발생했는지
  • rkB/s | 초당 읽은 데이터 양 | KiB/s | read 대역폭 (읽기 속도)
  • rrqm/s | 초당 병합된(read merged) 요청 수 | req/s | 요청 큐에서 병합된 read 요청 수
  • %rrqm | 전체 read 중 병합된 비율 | % | 높으면 read 요청이 효율적으로 병합되고 있음
  • r_await | 평균 read 요청 대기 시간 | ms | I/O 요청 → 완료까지 걸린 평균 시간 (대기 + 서비스)
  • rareq-sz | 평균 read 요청 크기 | KiB | 평균 한 번의 read 요청이 몇 KiB인지
  • w/s | 초당 쓰기 요청 수 (writes per second) | req/s | 초당 몇 번의 write 요청이 발생했는지
  • wkB/s | 초당 쓴 데이터 양 | KiB/s | write 대역폭 (쓰기 속도)
  • wrqm/s | 초당 병합된(write merged) 요청 수 | req/s | 요청 큐에서 병합된 write 요청 수
  • %wrqm | 전체 write 중 병합된 비율 | % | 높을수록 요청 병합이 잘 이루어짐
  • w_await | 평균 write 요청 대기 시간 | ms | I/O 요청 → 완료까지 걸린 평균 시간
  • wareq-sz | 평균 write 요청 크기 | KiB | 평균 한 번의 write 요청 크기
  • d/s | discard 요청 수 (trim, discard 등) | req/s | SSD에서 블록 해제(discard/TRIM) 빈도
  • dkB/s | 초당 discard된 데이터 양 | KiB/s | discard 작업 데이터량
  • drqm/s | 초당 병합된 discard 요청 수 | req/s | discard 요청 병합 빈도
  • %drqm | 전체 discard 중 병합된 비율 | % | 보통 SSD에서만 유효
  • d_await | 평균 discard 요청 지연 시간 | ms | discard 완료까지의 시간
  • dareq-sz | 평균 discard 요청 크기 | KiB | 한 번 discard당 크기
  • f/s | flush 요청 수 | req/s | 파일시스템의 flush (캐시 → 디스크) 요청 빈도
  • f_await | 평균 flush 대기 시간 | ms | flush 요청 완료까지 걸린 시간
  • aqu-sz | 평균 I/O 대기열 길이 (average queue size) | 요청 수 | 큐에 평균 몇 개의 I/O가 대기 중인지
  • %util | 장치 사용률 | % | 장치가 I/O 처리로 바쁘게 사용된 시간 비율 (100%면 항상 바쁨)

Email 답글이 올라오면 이메일로 알려드리겠습니다.