- [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%)
```
## 추가 정보
[//]: # (이슈와 관련된 추가적인 정보가 있다면 기술해주세요. 예: 스크린샷, 로그 파일 등)
- 완료 안된것들 테스트로 실행
이슈 개요
트랜잭션 동시성 테스트
재현 단계
파이썬 코드
- [x] 2. 테스트 시나리오 작성 - 각 수단 별 성능 비교 - [x] 3. 개선 전 테스트 - jmeter, pymter? - [ ] 4. 락 - [ ] 낙관, 비관 - [ ] 네임드 락 - [ ] Redis 락 - [ ] 5. Jpa Cache - [x] 6. 트랜잭션 격리수준 - [x] 7. 다른 방법 - [x] #10 ## 예상 동작 [//]: # (올바른 상황에서 기대하는 동작이 무엇인지 기술해주세요.) 1. 덤프 데이터로 테스트 실행 2. 문제 발생 3. 락 / Cache / 격리수준 등으로 테스트 ## 실제 동작 [//]: # (문제가 발생했을 때의 실제 동작을 기술해주세요.) 1. 시나리오 작성
2. 덤프 데이터로 테스트 실행
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%)
```
## 추가 정보
[//]: # (이슈와 관련된 추가적인 정보가 있다면 기술해주세요. 예: 스크린샷, 로그 파일 등)
- 완료 안된것들 테스트로 실행
작성한 테스트 시나리오
- **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개?
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(); } } ```