O0oO0Oo / netty-reservation-service

트랜잭션, 동시성을 공부하기 위한 토이 프로젝트입니다.
0 stars 0 forks source link

feat: concurrent transaction test #9

Closed O0oO0Oo closed 3 weeks ago

O0oO0Oo commented 3 months ago

이슈 개요

트랜잭션 동시성 테스트

재현 단계

import mysql.connector
from datetime import datetime, timedelta

db = mysql.connector.connect(
    host="192.168.35.179",
    user="master",
    password="master",
    database="reservation"
)

cursor = db.cursor()

def create_business(name, business_type):
    return (name, business_type)

def create_user(name, balance):
    return (name, balance)

def create_reservable_item(name, quantity, max_quantity_per_user, days_from_now, price, business_id, is_available):
    reservable_time = (datetime.now() + timedelta(days=days_from_now)).strftime('%Y-%m-%d %H:%M:%S')
    return (name, quantity, max_quantity_per_user, reservable_time, price, business_id, is_available)

def user_balance_case(user_cnt):
    if user_cnt <= 10000:
        return 10000
    elif user_cnt <= 30000:
        return 2000
    else:
        return 1000

def item_case(item_cnt):
    if item_cnt < 11:
        return (5, 5, 10)
    elif item_cnt < 201:
        return (10, 3, 1000)
    elif item_cnt < 301:
        return (5, 2, 1000)
    else:
        return (2, 2, 1000)

def insert_data():
    # User 5만명
    for i in range(1, 50001):
        balance = user_balance_case(i)
        user_data = create_user("user" + str(i) , balance)
        cursor.execute("INSERT INTO user (name, balance) VALUES (%s, %s)", user_data)
        db.commit()

    item_count = 0
    # business
    for i in range(1, 101):
        business_data = create_business("business" + str(i), "STORE")
        cursor.execute("INSERT INTO business (name, business_type) VALUES (%s, %s)", business_data)
        business_id = cursor.lastrowid

        # item
        for j in range(1, 101):
            item_count += 1
            quantity, max_quantity_per_user, price = item_case(item_count)
            reservable_item_data = create_reservable_item("item" + str(i), quantity, max_quantity_per_user, 100, price, business_id, True)
            cursor.execute("""
                INSERT INTO reservable_item (name, quantity, max_quantity_per_user, reservable_time, price, business_id, is_available) 
                VALUES (%s, %s, %s, %s, %s, %s, %s) """, reservable_item_data)

        db.commit()

insert_data()

cursor.close()
db.close()

- [x] 2. 테스트 시나리오 작성 - 각 수단 별 성능 비교 - [x] 3. 개선 전 테스트 - jmeter, pymter? - [ ] 4. 락 - [ ] 낙관, 비관 - [ ] 네임드 락 - [ ] Redis 락 - [ ] 5. Jpa Cache - [x] 6. 트랜잭션 격리수준 - [x] 7. 다른 방법 - [x] #10 ## 예상 동작 [//]: # (올바른 상황에서 기대하는 동작이 무엇인지 기술해주세요.) 1. 덤프 데이터로 테스트 실행 2. 문제 발생 3. 락 / Cache / 격리수준 등으로 테스트 ## 실제 동작 [//]: # (문제가 발생했을 때의 실제 동작을 기술해주세요.) 1. 시나리오 작성

작성한 테스트 시나리오

- **quantity(q)** - 남은 수량 - **max_quantity_per_user(mq)** - 유저 당 최대 구매 가능 수량 - **Price(p)** - 가격 - **참고 - Business id 가 2번 부터 시작 됨.** - **유저 50000 명 - user{number 1 ~ 50000}** - 유저 1 ~ 10000 : 잔고 10000 - 유저 10001 ~ 30000 : 잔고 2000 - 유저 30001 ~ 50000 : 잔고 1000 - **회사 100 개 - business{number 1 ~ 1000}** - **아이템 회사 당 100 개, 총 10000 개** - 아이템 번호 1 ~ 10 까지는 각 q - 5, mq - 5, p - 10 총 수량 50개 - 아이템 번호 101 ~ 200 까지는 각 q - 10, mq - 3, p - 1000 총 수량 200개 - 아이템 번호 201 ~ 300 까지 각 q - 5, mq - 2, p - 1000 총 수량 200개 - 그외 q - 2, mq -2, p 1000 ### 케이스 - 각 케이스 별로 측정해야 함. 1. **트랜잭션 동시성** - 유저 1 ~ 10000 : 아이템 1 ~ 10 번까지 1000 명이 각 레코드 번호에 2번 요청 1. 결과 : 유저 1 ~ 10000 번까지 총 예약 레코드 수 50개가 나와야 함. 2. 성능 : 모든 요청이 끝나는 시간. 2. **데이터 정합성 1** - 유저 10001 ~ 30000 : 아이템 101 ~ 200 잔액 부족해질 때까지 요청 후 모두 취소 - 유저 200 단위로 1개의 레코드 요청, 없다면 멈춤 1. 결과 1. 유저 10001 ~ 30000 : 예약 레코드 ?개, 잔고 2000, 아이템 남은 수량 10 2. 성능 : 데이터 정합성 3. **데이터 정합성 2** - 유저 30001 ~ 40000 : 아이템 201 ~ 300 예약 후 취소 - 유저 100 단위로 레코드 1개 - 유저 40001 ~ 50000 : 아이템 201 ~ 300 예약 - 유저 100 단위로 레코드 1개 1. 결과 1. 유저 30001 ~ 40000 : 잔고 1000, 예약 레코드 ? 2. 유저 40001 ~ 50000 : 잔고 0 또는 1000, 예약 레코드 ? 3. 아이템 201 ~ 300 남은 수량 0 2. 성능 : 데이터 정합성 4. **부하 테스트 - 1, 2 초기화 후** 유저 1 ~ 50000 : 아이템 번호 1 ~ 101 까지 한번 씩 요청 1. 결과 : 모든 유저 총 예약 레코드 500개?

2. 덤프 데이터로 테스트 실행
Jmeter 코드

```java /** * 유저 10000명 * 1 ~ 1000 번의 유저 1번 아이템 예약 요청 * 1001 ~ 2000 번의 유저 2번 아이템 예약 요청 * .... * 9001 ~ 10000 번의 유저 10번 아이템 예약 요청 * * 1 ~ 10번 아이템은 각 5개씩 총 50개가 있다. * * */ public class Main { public static void main(String[] args) throws IOException { JMeterUtils.setJMeterHome("E:/apache-jmeter-5.6.3/apache-jmeter-5.6.3"); JMeterUtils.loadJMeterProperties("E:/apache-jmeter-5.6.3/apache-jmeter-5.6.3/bin/jmeter.properties"); JMeterUtils.initLocale(); StandardJMeterEngine jmeter = new StandardJMeterEngine(); TestPlan testPlan = new TestPlan("Concurrent Transaction Test Plan"); CSVDataSet csvDataSet = new CSVDataSet(); String csvFilePath = Main.class.getResource("/post_user10000.csv").getPath(); csvDataSet.setProperty("filename", csvFilePath); csvDataSet.setProperty("variableNames", "user_id,businessId,itemId,quantity"); csvDataSet.setProperty("delimiter", ","); csvDataSet.setProperty("recycle", "true"); csvDataSet.setProperty("stopThread", "false"); csvDataSet.setProperty("shareMode", "shareMode.all"); HTTPSamplerProxy registerHttpSampler = new HTTPSamplerProxy(); registerHttpSampler.setDomain("localhost"); registerHttpSampler.setPort(8080); registerHttpSampler.setPath("/user/${user_id}/reservation"); registerHttpSampler.setMethod("POST"); String jsonBody = "{ \"businessId\": \"${businessId}\", \"itemId\": \"${itemId}\", \"quantity\": \"${quantity}\" }"; registerHttpSampler.addNonEncodedArgument("", jsonBody, "="); registerHttpSampler.setPostBodyRaw(true); HeaderManager headerManager = new HeaderManager(); headerManager.add(new Header("Content-Type", "application/json")); registerHttpSampler.setHeaderManager(headerManager); LoopController loopController = new LoopController(); loopController.setLoops(1); loopController.addTestElement(registerHttpSampler); loopController.setFirst(true); loopController.initialize(); ThreadGroup threadGroup = new ThreadGroup(); threadGroup.setName("ConcurrentTransactionTest"); threadGroup.setNumThreads(10000); threadGroup.setRampUp(1); threadGroup.setSamplerController(loopController); HashTree testPlanTree = new ListedHashTree(); HashTree threadGroupTree = testPlanTree.add(testPlan); threadGroupTree.add(threadGroup); threadGroupTree.add(csvDataSet); threadGroupTree.add(registerHttpSampler); Summariser summer = null; String summariserName = JMeterUtils.getPropDefault("summariser.name", "summary"); if (summariserName.length() > 0) { summer = new Summariser(summariserName); } String logFile = "concurrent_transaction_test_results.jtl"; ResultCollector logger = new ResultCollector(summer); logger.setFilename(logFile); testPlanTree.add(testPlanTree.getArray()[0], logger); SaveService.saveTree(testPlanTree, new FileOutputStream("concurrent_transaction_test.jmx")); jmeter.configure(testPlanTree); jmeter.run(); } } ```

3. 문제 발생 - 유저 10000명이 50개의 예약 아이템을 두고 요청 -> 50개에 대한 예약 기록이 생겨야 하며 50개가 성공해야하지만 아래와 같이 69개가 성공하였다. ```python summary + 6421 in 00:00:24 = 262.8/s Avg: 11887 Min: 683 Max: 22988 Err: 6357 (99.00%) Active: 3578 Started: 10000 Finished: 6422 summary + 3579 in 00:00:09 = 380.5/s Avg: 21810 Min: 16909 Max: 27711 Err: 3574 (99.86%) Active: 0 Started: 10000 Finished: 10000 summary = 10000 in 00:00:34 = 295.5/s Avg: 15438 Min: 683 Max: 27711 Err: 9931 (99.31%) org.apache.jmeter.reporters.ResultCollector@cde3eeb7 ``` - 트랜잭션 간 데드락 발생 ```python 2024-06-10T09:29:57.591+09:00 WARN 223492 --- [reservation] [tLoopGroup-1-11] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1213, SQLState: 40001 ... 2024-06-10T09:29:57.591+09:00 ERROR 223492 --- [reservation] [ntLoopGroup-1-8] o.h.engine.jdbc.spi.SqlExceptionHelper : Deadlock found when trying to get lock; try restarting transaction ... 2024-06-10T09:29:58.501+09:00 ERROR 223492 --- [reservation] [tLoopGroup-1-14] c.s.r.n.http.handler.ExceptionHandler : Exception, could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update reservable_item set business_id=?,is_available=?,last_modified_time=?,max_quantity_per_user=?,name=?,price=?,quantity=?,reservable_time=? where item_id=?]; SQL [update reservable_item set business_id=?,is_available=?,last_modified_time=?,max_quantity_per_user=?,name=?,price=?,quantity=?,reservable_time=? where item_id=?] ``` 4. 진행중 - Transaction - Transaction Id - #10 6. 결과 - Transaction - Isolation.SERIALIZABLE 해결되지만 SERIALIZABLE 수준은 동시처리 성능이 가장 낮음 ```python > Task :Main.main() summary + 5774 in 00:00:26 = 223.0/s Avg: 11818 Min: 1200 Max: 19758 Err: 5729 (99.22%) Active: 4227 Started: 10000 Finished: 5773 summary + 4226 in 00:00:14 = 295.4/s Avg: 24450 Min: 17289 Max: 31055 Err: 4221 (99.88%) Active: 0 Started: 10000 Finished: 10000 summary = 10000 in 00:00:40 = 248.8/s Avg: 17156 Min: 1200 Max: 31055 Err: 9950 (99.50%) ``` ## 추가 정보 [//]: # (이슈와 관련된 추가적인 정보가 있다면 기술해주세요. 예: 스크린샷, 로그 파일 등) - 완료 안된것들 테스트로 실행