Artillery를 사용한 Stress Testing(DB Connection 데드락)

Belluga·2021년 10월 5일
0

Artillery

REST API 백엔드 시스템에서 Stress Testing을 위해 Artillery를 사용해보도록 하겠습니다.

Artillery 설치

https://nodejs.org/en/download/
먼저 위 링크를 통해 node.js, NPM 설치를 진행합니다.
설치가 완료되면 아래와 같이 버전을 확인할 수 있습니다.

npm을 통해 아래 명령어로 Artillery를 설치합니다.
공식 문서처럼 전역으로 설치하였습니다.

npm install -g artillery@latest

아래 명령어로 설치를 확인할 수 있습니다.🦖

artillery dino

Artillery 예제 테스트 스크립트 살펴보기

Arillery 테스트 스크립트는 JSON, YAML 파일로 작성할 수 있습니다.
해당 파일은 크게 configuration 부분과 scenarios 부분으로 나눌 수 있습니다.

🔧 configuration
: 성능 테스트를 위한 구성을 설정합니다.

config:
  target: "https://example.com/api"
  phases:
    - duration: 60
      arrivalRate: 5
      name: Warm up
  payload:
    path: "keywords.csv"
    fields:
      - "keyword"    

config.target : 테스트하려는 애플리케이션의 URL을 설정합니다.

config.phases : 얼마나 많은 가상의 사용자가 얼마나 자주 서버에 트래픽 요청을 보낼것인지 설정할 수 있습니다.
위 예시에서는 60초간 매초 5명의 가상의 유저가 생성됩니다.

config.payload : Artillery에서 외부 CSV 파일을 로드할 수 있습니다. 로드한 데이터를 변수에 담아 테스트 시나리오에서 사용할 수 있습니다.

🎆 scenarios
: 여러 시나리오를 정의할 수 있으며 각 시나리오에는 하나 이상의 작업이 포함됩니다.

scenarios:
  - name: "Search and buy"
    flow:
      - post:
          url: "/search"
          json:
            kw: "{{ keyword }}"

위 예시 시나리오에서는 각 가상 유저가 HTTP POST 요청을 생성합니다.
해당 요청의 body는 json 데이터를 갖는데 데이터 값은 외부에서 로드한 CSV 파일의 데이터를 셋팅합니다.

keyword.csv

computer
video game
vacuum cleaner

예를 들어 위 keyword.csv 파일을 로드했을 때, 각 가상 사용자는 아래 데이터를 요청 데이터로 셋팅합니다.
{"kw": "computer"}, {"kw": "video game"}, {"kw": "vacuum cleaner"}

Json 테스트 스크립트

 {
    "config": {
        "target": "http://localhost:8080",
        "phases": [
            {"duration": 20, "arrivalRate": 10}
        ],
        "defaults": {
            "headers": {
                "User-Agent": "Artillery"
            }
        },
		"payload": {
			"path": "./data.csv",
			"order": "sequence",
			"fields": ["email", "password", "name", "phone"]
		}
    },
    "scenarios": [
        {
            "name": "유저생성테스트",
            "flow": [
                {"post":
                    {
                        "url": "/api/users",
                        "json": {"email": "{{email}}", "password": "{{password}}", "name" : "{{name}}", "phone" : "{{phone}}" }
                    }
                }
            ]
        }
    ]
}

회원가입 테스트를 위한 시나리오를 Json 파일로 작성하였습니다.

참고로 외부 파일을 load하는 경우 데이터를 랜덤하게 선택해서 사용하기 때문에 중복되는 요청을 할 수 있습니다.

해당 서비스에서는 이메일을 아이디처럼 사용하여 이메일이 중복되면 회원가입을 할 수 없기 때문에 "order": "sequence" 옵션을 추가하여 sequencial하게 데이터를 선택하도록 하였습니다.

테스트 스크립트 실행

artillery run test.json

위 명령어를 통해 작성한 스크립트를 실행할 수 있습니다.

문제점 발생

20초간 매초 10명의 가상의 유저가 '유저 생성 테스트'를 진행했을 때 문제가 발생하였습니다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue
    @Column(name = "user_id")
    private Long id;
    
    ...
}

현재 User 엔티티의 id 필드에 @GeneratedValue 어노테이션을 설정하였는데 해당 어노테이션의 기본값은 GenerationType.AUTO입니다.

Hibernate 5.0부터 MySQL의 AUTO는 IDENTITY가 아닌 TABLE을 기본 시퀀스 전략을 사용합니다.

이는 공용 시퀀스 테이블(hibernate_sequence)을 두고 모든 테이블의 id 시퀀스를 한 테이블에서 관리하는 방식입니다.

select
    next_val as id_val 
from
    hibernate_sequence for update

update
    hibernate_sequence 
set
    next_val= ? 
where
    next_val=?

insert into users (created_at, modified_at, email, name, password, phone, role, user_id) values (?, ?, ?, ?, ?, ?, ?, ?)

hibernate_sequence 공용 시퀀스 테이블은 최신 시퀀스 값을 유지하며 조회한 row에 lock을 걸어 두 개의 트랜잭션이 동일한 시퀀스 값을 획득하는 것을 방지합니다.

이때 장기 실행 트랜잭션이라면 다른 트랜잭션이 새 시퀀스 값을 획득함에 오랜 시간이 소요되기 때문에 ID를 select하고 update하는 별도의 트랜잭션을 사용합니다.

즉 하나의 스레드가 Connection 2개를 획득해야하는 TABLE 전략은 상당한 성능 오버헤드를 수반하고 특히 부하가 심할 때 커넥션을 획득하는데 문제가 생기는 것을 확인할 수 있었습니다.

부하가 심한 경우 각 스레드가 Connection을 획득하여 남아있는 Idle Connection이 없는 상황에서 추가적인 2차 Connection을 얻으려고 하기 때문에 모든 스레드가 Connection을 얻을 수 없는 데드락 문제가 발생할 것입니다.

@GeneratedValue(strategy = GenerationType.IDENTITY)

@GeneratedValue 어노테이션의 전략을 GenerationType.IDENTITY로 변경해준 결과 timeout 문제를 해결할 수 있었습니다.

References

https://artillery.io/docs/guides/getting-started/installing-artillery.html

https://jojoldu.tistory.com/295

https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/

https://techblog.woowahan.com/2663/

0개의 댓글