Posting

Machbase의 최신 소식을 지금 만나보세요

[MACHBASE Go] CGO

cgo 로 마크베이스와 go 프로그램을 연동해 보자

마크베이스 연구개발본부 김정용

들어가며

go 언어는 유연한 개발 환경, 손쉬운 모듈화, 빠른 컴파일 성능, Non-blocking 동시성 기능 이라는 장점을 가지고 있는 언어이다.
따라서 마이크로서비스에 효과적이고, 성능에 대비해 개발 생산성이 좋은 언어로 알려져 있다.
최근 IoT 엣지 오픈소스인 EdgeX Foundry에도 go 언어가 사용되었으며 시시각각 급변하는 IoT 필드에서 go 언어의 시너지가 좋을 것으로 보인다.

4차 산업혁명의 화두가 되고 있는 산업용 IoT 데이터를 다루는 마크베이스에서도, 이 IoT 엣지 시스템에 도입되고 있는 go 언어와 연동하려는 시도를 시작했다.
하지만, 다양한 언어에 대한 소프트웨어 개발 도구 (SDK) 를 제공하고는 있지만 아직 go SDK 는 개발하는데 많은 시간이 소요되었다.

그래서 방안을 고민하던 중 cgo 를 선택해 연동하는 방법을 고안하게 되었다.
cgo 는 go 에서 지원하는 커맨드로, go 코드상에 C 코드를 바인딩해서 함수나 변수를 호출할 수 있도록 한다.
cgo 를 활용하면 C 기반의 라이브러리, 알고리즘, 기존 레거시 코드 등을 특별한 변환없이 go 에서 바로 사용할 수 있게 된다.
하지만, cgo는 go 환경에서 돌아가는 구조이기 때문에 기존 C 언어에 비해 성능 차이가 있을 수 있다.

이런 배경을 바탕으로 이번 포스팅에서는 cgo 의 기본 사용법과 마크베이스에서의 cgo 활용, 성능 비교 등에 대한 내용들을 정리했다.

기본 사용법

cgo 의 기본 사용법은, go 코드에서 “C” 패키지를 import하고 바로 위에 빈 공간이 없이 c 코드를 주석 처리하면 된다.
그리고 C.함수명 or C.변수명 과 같이 필요한 C 함수나 변수를 호출한다.

package main
/*
C코드 ...
*/
import "C"
import "fmt"
func main() {
        fmt.Println(C.함수)
        fmt.Println(C.변수)
}

go 에서 C 함수 호출

다음 코드는, 두 정수의 합을 연산하고 리턴하는 C 함수를 호출하는 예제다.
여기서 주목해야할 점은 go 와 C 의 자료형이 다르기 때문에, 형변환 (type casting) 이 필요하다는 점이다.
여기서는 C 에서 반환된 변수 값을 출력하므로, C.int() 를 이용해 C 정수형 자료형으로 형변환 해야 한다.

package main
/*
#include 
int sum(int a, int b) {
        return a + b;
}
*/
import "C"
import "fmt"
 
func main() {
        a := C.int(10)
        b := C.int(20)
        res := C.sum(a, b)
        fmt.Println(C.int(res))
}

C 에서 go 함수 호출

반대로, C 레벨 코드에서 go 코드를 호출할 수도 있다!
유의할 점은 C 로부터 호출 될 go 함수 바로 위에 “//export 함수명” 주석이 추가 되어야 하며 c 에서 go 를 호출하는 c 함수는 static inline 형태로 선언되어야 한다.

package main
/*
#include 
extern int sum(int a, int b);
static inline void cExample() {
        int res = sum(10, 20); // go 언어의 sum 함수 호출
        printf("%d\n", res);
}
*/
import "C"
 
//export sum
func sum(a, b C.int) C.int {
        return a + b
}
 
func main() {
        C.cExample()
}

또한 이렇게 선언된 static inline 함수에서 호출되는 다른 c 함수들 역시 static 기반이어야 한다.

package main
/*
#include 
extern int sum(int a, int b);
 
static void resPrint(int res) {
        printf("%d\n", res);
}
static inline void cExample() {
        int res = sum(10, 20); // go 언어의 sum 함수 호출
        resPrint(res);
}
*/
import "C"
 
//export sum
func sum(a, b C.int) C.int {
        return a + b
}
 
func main() {
        C.cExample()
}

cgo 포인터 기본

go 는 포인터를 허용한다. 따라서 cgo 에서도 포인터를 활용할 수 있다.
다음은 go 에서 선언된 두 변수를 포인터를 통해 cgo 에서 값을 바꾸는 예제다.

package main
/*
#include 
void valueChange(int* a, int* b) {
        *a = 30;
        *b = 40;
}
*/
import "C"
import "fmt"
func main() {
        a := C.int(10)
        b := C.int(20)
        fmt.Println("a : ", C.int(a))
        fmt.Println("b : ", C.int(b))
        C.valueChange(&a, &b)
        fmt.Println("a : ", C.int(a))
        fmt.Println("b : ", C.int(b))
}
$ go run pointerExample.go
a :  10
b :  20
a :  30
b :  40

cgo 포인터 응용

cgo 의 포인터를 응용해서 c 구조체 자료형의 go 슬라이스 를 생성하고 cgo 로 구조체 배열 형태의 포인터로 넘겨줄 수 있다.
이것은 go 와 cgo 간에 배열 데이터를 공유 할 때 유용하다.
하지만 go 에서 슬라이스를 생성할 때 선언한 크기만큼만 포인터를 참조시킬 수 있기 때문에 크기가 0인 구조체 배열 포인터 형태로 넘길 수 없으며, cgo 에서 realloc 을 통한 사이즈 조정 역시 불가능하다.
즉 초기 go 에서 슬라이스를 생성할 때 크기 만큼만 데이터를 참조시킬 수 있다.
주의할 부분은, cgo 에서 할당한 자원들은 go 의 Garbage Collector 가 관리하지 않는다는 점이다. 따라서, 슬라이스에 동적 할당된 메모리는 free를 통해 반드시 해제해야 한다.

package main
/*
#include 
#include 
typedef struct {
    short                seq;
    char                 id[11];
    int                  score;
    long                 total;
    float                percentage;
    double               ratio;
}Data;
void pointerExample(Data** dataList, int dataListLength) {
        int i;
        for(i=0; iseq = i;
                sprintf(dataList[i]->id, "id_%d", i);
                dataList[i]->score = 100 + i;
                dataList[i]->total = 1000000000 + i;
                dataList[i]->percentage = 10.0 + i;
                dataList[i]->ratio = 20.0 + i;
        }
}
*/
import "C"
import (
        "fmt"
        "unsafe"
)
func main() {
        dataList := make([]*C.Data, 10)
        defer func() {
                // free C struct array
                for _, data := range dataList {
                        C.free(unsafe.Pointer(data))
                }
        }()
        // Create go slice of C struct array
        for i, _ := range dataList {
                p := (*C.Data)(C.malloc((C.size_t(C.sizeof_Data))))
                dataList[i] = p
        }
        C.pointerExample(&dataList[0], C.int(len(dataList)))
        for _, data := range dataList {
                fmt.Println(data)
        }
}

Machbase – cgo 활용

다음은 cgo 를 활용해 Machbase 서버에 데이터를 입력하고 조회하는 예제를 나타낸 것이다.

환경 변수 설정

machbase C/ODBC 라이브러리를 추가해야 한다.
machbase 가 설치된 경로를 $MACHBASE_HOME 환경변수로 설정했다고 가정하고, 다음 환경 변수를 추가한다.

export LD_LIBRARY_PATH=$MACHBASE_HOME/lib:$LD_LIBRARY_PATHexport CGO_CFLAGS="-I$MACHBASE_HOME/include"
export CGO_LDFLAGS="-L$MACHBASE_HOME/lib -lmachbasecli_dll"

소스

– 소스 다운로드(Github)
– 소스 다운로드 후 각 디렉토리에서 make
– 생성된 append, select 바이너리 파일 실행
– append 더미 데이터 행 수 설정은 make_data.c 파일의 MAX_DATA_COUNT 를 조정하면 된다. (기본값 1000)

export LD_LIBRARY_PATH=$MACHBASE_HOME/lib:$LD_LIBRARY_PATH
export CGO_CFLAGS="-I$MACHBASE_HOME/include"
export CGO_LDFLAGS="-L$MACHBASE_HOME/lib -lmachbasecli_dll"

스키마

create table CLI_SAMPLE(SEQ         short,
                        SCORE       integer,
                        TOTAL       long,
                        PERCENTAGE  float,
                        RATIO       double,
                        ID          varchar(13),
                        SRCIP       ipv4,
                        DSTIP       ipv6,
                        REG_DATE    datetime,
                        TEXTLOG     text,
                        IMAGE       binary);

입력 – APPEND

생성된 더미 데이터 파일로부터 행들을 읽어와 append한다.

조회 – SELECT

append된 데이터들을 조회하기 위해 select 쿼리를 진행한다.

성능 비교

위 소스를 기준으로 회당 1천만건의 레코드를 append 하고 select 하는 테스트를, c, cgo 각각 10회씩 수행하는 시나리오로 성능 비교 테스트를 진행했다.
테스트 방법은 더미데이터를 1천만건으로 설정 후 bash test_script.sh 명령어를 실행한다.

하드웨어

OSCPURAM
Ubuntu 18.04.2 LTSIntel(R) Xeon(R) CPU E3-1231 v3 @ 3.40GHz 8Core32GB

테스트 결과

 CCGOΔ
select25.3926.50104.37 %
append30.5147.31155.06 %

– append는 C 평균 30.51초, cgo 평균 47.31초로 약 1.5배 가량의 차이가 있었다.
– select는 C 평균 25.39초, cgo 평균 26.50초로 차이가 미미했다.

마치며

이번 과정을 통해 하나의 언어가 다른언어와의 호환을 통해 개발 생산성을 향상시킬 수 있었다는 점에서 언어의 새로운 패러다임을 느낄 수 있었다.
마크베이스 또한 c의 활용도가 높은 상황이었던 시점에 go라는 신규 언어를 보다 효율적이게 도입할 수 있었던 사례가 되었다.
이처럼, c 기반 인프라에서 성능 감소를 어느정도 감수하고 무언가를 빠르게 도입해서 시도해 볼 필요가 있다면 cgo 를 이용해보는것도 좋다.

연관 포스트

C언어로 Binary data를 Machbase에 넣기

1.개요 Data를 다루다 보면 numeric, varchar 형 데이터뿐만 아니라 JPG, PNG, MP3와 같은 Binary data도 database에 저장해야 할 때가 존재한다. 그러나 일반 data들과는 달리 Binary

[MACHBASE 연동] Android studio에서 JDBC 연결하기

마크베이스 기술지원본부 이현민 1. 개요 수많은 데이터들이 많은 환경에서 생성되고 있는 오늘날, 우리 현대인들의 동반자인 스마트폰 또한 데이터생성의 주체로써 또는 전달자로서 알게 모르게 그 구실을