성능 테스트 메뉴얼 - SeungpilPark/uEngine-bill GitHub Wiki

대용량 카달로그 전송

mysql 에서 다음의 에러가 남으로...

java.sql.SQLSyntaxErrorException: Row size too large (> 8126). Changing some columns to TEXT or BLOB may help

mysql 디렉토리의 my.cnf 파일을 다음으로 수정한다.

[mysqld]
.
.
max_allowed_packet=500M

innodb_file_per_table
innodb_file_format = Barracuda
innodb_log_file_size = 256M

이후 mysql 의 tenant_kv 테이블을 업데이트한다.

ALTER TABLE tenant_kvs     ENGINE=InnoDB     ROW_FORMAT=COMPRESSED      KEY_BLOCK_SIZE=8;

이후 http://localhost:8080/1.0/kb/catalog 엔드포인트로 POST 전송을 한다.

전문을 보낼때의 헤더규칙.

String auth = this.user + ":" + this.password;
        BASE64Encoder encoder = new BASE64Encoder();
        String encode = encoder.encode(auth.getBytes());

        Map mergeHeaders = new HashMap();

mergeHeaders.put("Authorization", "Basic " + encode);
        mergeHeaders.put("Content-Type", "application/xml");
        mergeHeaders.put("Accept", "application/json");
        mergeHeaders.put("X-Killbill-CreatedBy", "OpenBill");
        mergeHeaders.put("X-Killbill-ApiKey", this.apiKey);
        mergeHeaders.put("X-Killbill-ApiSecret", this.apiSecret);

대용량 카달로그 업데이트시 소요시간

상품 수 상품 당 플랜 수 총 플랜 수 용량(KB) 카달로그 생성 소요 시간(s) 카달로그 전송과 서버처리 시간(s) WAS 환경
10000 1 10000 5734 4 12 Xmx 2G
20000 1 20000 10686 9 34 Xmx 2G
30000 1 30000 15637 18 51 Xmx 2G
40000 1 40000 20588 42 172 Xmx 2G
50000 1 50000 25539 79 369 Xmx 2G
150000 1 150000 86714 298 1182 Xmx 2G

카달로그 버전 중첩과 성능

데일리마다 한번씩 대용량 카달로그가 업데이트 된다는 가정하에, 카달로그 버젼을 업데이트 해가며 서브스크립션 차지 계산 및 인보이스 생성 능력을 테스트함.

카달로그 버젼 수 전체 플랜 수 전체 서브스크립션 수 인보이스 당 서브스크립션 수 인보이스 처리 속도(분당) 모니터링 시간(분)
2 20000 10000 2 201 5
4 20000 10000 2 212 5
6 20000 10000 2 서버 정지 5

카달로그가 6개째가 되면서 2G 를 가지고 있는 was 가 힙메모리 부족이 뜨면서 죽음.

에러 원인은 서브스크립션 계산을 위해 카달로그 캐쉬를 사용하는 것과는 별개로, 예전 버전의 가격변동 여부를 확인하기 위해 데이터베이스에서 카달로그를 가져오는 과정에 힙메모리가 걸림.

Caused by: java.lang.OutOfMemoryError: Java heap space
	at java.lang.StringCoding$StringDecoder.decode(StringCoding.java:149)
	at java.lang.StringCoding.decode(StringCoding.java:193)
	at java.lang.String.<init>(String.java:426)
	at java.lang.String.<init>(String.java:491)
	at org.mariadb.jdbc.internal.common.AbstractValueObject.getString(AbstractValueObject.java:82)
	at org.mariadb.jdbc.internal.mysql.MySQLValueObject.getString(MySQLValueObject.java:78)
	at org.mariadb.jdbc.MySQLResultSet.getString(MySQLResultSet.java:131)
	at com.zaxxer.hikari.proxy.ResultSetJavassistProxy.getString(ResultSetJavassistProxy.java)
	at org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapper.map(LowerToCamelBeanMapper.java:126)
	at org.skife.jdbi.v2.RegisteredMapper.map(RegisteredMapper.java:37)
	at org.skife.jdbi.v2.Query$4.munge(Query.java:183)
	at org.skife.jdbi.v2.QueryResultSetMunger.munge(QueryResultSetMunger.java:43)
	at org.skife.jdbi.v2.SQLStatement.internalExecute(SQLStatement.java:1340)
	at org.skife.jdbi.v2.Query.fold(Query.java:173)
	at org.skife.jdbi.v2.Query.list(Query.java:82)
	at org.skife.jdbi.v2.sqlobject.ResultReturnThing$IterableReturningThing.result(ResultReturnThing.java:255)
	at org.skife.jdbi.v2.sqlobject.ResultReturnThing.map(ResultReturnThing.java:48)
	at org.skife.jdbi.v2.sqlobject.QueryHandler.invoke(QueryHandler.java:45)
	at org.skife.jdbi.v2.sqlobject.SqlObject.invoke(SqlObject.java:175)
	at org.skife.jdbi.v2.sqlobject.SqlObject$2.intercept(SqlObject.java:92)
	at org.skife.jdbi.v2.sqlobject.CloseInternalDoNotUseThisClass$$EnhancerByCGLIB$$ad218284.getTenantValueForKey(<generated>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperInvocationHandler$2.execute(EntitySqlDaoWrapperInvocationHandler.java:207)
	at org.killbill.commons.profiling.Profiling.executeWithProfiling(Profiling.java:33)
	at org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperInvocationHandler.invokeRaw(EntitySqlDaoWrapperInvocationHandler.java:204)
	at org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperInvocationHandler.invokeSafely(EntitySqlDaoWrapperInvocationHandler.java:199)
	at org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperInvocationHandler.access$000(EntitySqlDaoWrapperInvocationHandler.java:82)
	at org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperInvocationHandler$1.execute(EntitySqlDaoWrapperInvocationHandler.java:120)
	at org.killbill.commons.profiling.Profiling.executeWithProfiling(Profiling.java:33)

이를 통하여 하나의 xml 카달로그에 모든 제품을 담는 방식은 회피하고, WorkAround 코드를 작성하여 운용해야 한다는 것을 알 수 있다.

쓰레드별(스트레스 테스트) 성능 측정

사용자 생성 api 호출을 다음과 같이 돌렸을 때 성능 측정.

환경은 모두 local. 쓰레드가 100 이상일때 데이터 유실의 위험이 있으므로 쓰레드 제한을 걸어 운용해야 함을 알 수 있다.

쓰레드 반복횟수 총 횟수 완료된 데이터 건 소요시간(s) 호출당 소요시간(ms) 실패 사유 WAS 환경
1 10000 10000 10000(100%) 4121 412.1 Xmx 2G
10 1000 10000 10000(100%) 943 94.3 Xmx 2G
20 500 10000 10000(100%) 607 60.7 Xmx 2G
50 200 10000 10000(100%) 524 52.4 Xmx 2G
100 100 10000 9941(99.4%) 498 49.8 DB 인서트 실패 Xmx 2G

테스트 코드

public static void main(String[] args) throws Exception {

        LogOff.logOff();

        //포시에스 테넌트에 유저(구매자) 1만명 등록
        //100명씩 100개 쓰레드로 접속.
        int userNumbers = 100;
        int thredsNumbers = 100;

        for (int i = 0; i < thredsNumbers; i++) {
            int startCount = i * userNumbers + 1;
            int endCount = startCount + userNumbers;

            CreateAcount test = new CreateAcount(startCount, endCount);
            test.start();
        }
    }

    public static class CreateAcount extends Thread {
        int startCount;
        int endCount;

        public CreateAcount(int startCount, int endCount) {
            this.startCount = startCount;
            this.endCount = endCount;
        }

        public void run() {
            String user = "admin";
            String password = "password";
            String apiKey = "forcs";
            String apiSecret = "forcs";
            String host = "localhost";
            int port = 8080;

            KillbillApi api = new KillbillApi(host, port, user, password, apiKey, apiSecret);
            for (int c = startCount; c < endCount; c++) {
                String name = "test-user-" + c;
                String email = name + "@gmail.com";
                String currency = "USD";
                String externalKey = name;
                int billCycleDayLocal = 5;
                api.createAccount(name, email, currency, billCycleDayLocal, externalKey);
            }
        }
    }
⚠️ **GitHub.com Fallback** ⚠️