ํ์ฌ ์๋น์ค์์๋ ์ฌ๊ณ ์๋์ ๊ด๋ฆฌํ๋ ๋ก์ง์ด ์กด์ฌํ๋ฉฐ,
์ฌ๋ฌ ์์ฒญ์ด ๋์์ ๋ค์ด์ฌ ๊ฒฝ์ฐ ๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ๋ ๊ตฌ์กฐ๋ค.
์ด๋ฅผ ์ง์ ๋์ผ๋ก ํ์ธํ๊ณ ์ถ์ด์,
์ฌ๊ณ ์๋์ ๋จ์ํ 1 ๊ฐ์์ํค๋ API๋ฅผ ์์ฑํ๊ณ K6
๋ฅผ ํ์ฉํด ๋ถํ ํ
์คํธ๋ฅผ ์งํํ๋ค.
ํ ์คํธ ์คํฌ๋ฆฝํธ
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
scenarios: {
simultaneous_requests: {
executor: 'per-vu-iterations', // ๊ฐ VU๊ฐ ๋์์ ์์
vus: 10, // VU ์
iterations: 1, // ๊ฐ VU๊ฐ ํ ๋ฒ๋ง ์คํ
maxDuration: '1s', // ์ ์ฒด ์๋ฎฌ๋ ์ด์
์๊ฐ
},
},
};
export default function() {
http.post('http://localhost:8080/inventories/test');
}
k6 ์คํฌ๋ฆฝํธ ์คํ
# k6 run script.js
ํ ์คํธ ๊ฒฐ๊ณผ, 10๋ช ์ ์์ฒญ ์ค 1๊ฑด๋ง ๋ฐ์
์์๋๋ก Lost Update ํ์ ๋ฐ์
โ ๊ฐ์ฅ ๋ง์ง๋ง์ ์ปค๋ฐ๋ ์์ฒญ๋ง ๋ฐ์๋๊ณ , ๋๋จธ์ง๋ ๋ชจ๋ ๋ฌด์๋จ
@Transactional
ํ๊ฒฝ์์ ๋จ์ synchronized
๋ธ๋ก์ ์ฌ์ฉํด ๋ณด์์ง๋ง,
์ค๋ ๋ ๋ฝ์ ํ ํ๋ก์ธ์ค ๋ด์์๋ง ์ ํจํ๊ธฐ ๋๋ฌธ์ ๋ฉํฐ ํ๋ก์ธ์ค/๋ถ์ฐ ํ๊ฒฝ์์๋ ๋ฌด์๋ฏธํ๋ค.
๋ํ, ํธ๋์ญ์
๋ด๋ถ์์ ๋ฝ์ ๊ฑธ๋๋ผ๋,
์ปค๋ฐ ์์ ์ด์ ์ ๋ค๋ฅธ ์ค๋ ๋๊ฐ ๊ณผ๊ฑฐ ๊ฐ์ ์ฝ๋ ๋ฌธ์ ๊ฐ ์๊น โ ์คํจ
synchronized๋ JVM ๋ ๋ฒจ์ ๋ฝ
synchronized ํค์๋๋ Java ๊ฐ์ฒด ์์ค์์ ๋ฝ์ ๊ฑฐ๋ ๋ฉ์ปค๋์ฆ์ด๋ฉฐ
์ด ๋ฝ์ JVM ๋ด๋ถ์์ ๊ด๋ฆฌ๋๋ฉฐ, JVM ํ๋ก์ธ์ค ์์ ์๋ ์ฌ๋ฌ ์ค๋ ๋ ๊ฐ์ ๋์ ์ ๊ทผ์ ๋ง๋ ์ฉ๋๋ก ๋์
๋ฐ๋ผ์ JVM ๋ฐ, ์ฆ ๋ค๋ฅธ ํ๋ก์ธ์ค์์ ์คํ๋๋ ์ฝ๋์๋ ๋ฝ์ด ๊ณต์ ๋์ง ์์ ์ ํ ์ ์ดํ ์ ์์.
synchronized๋ JVM ๋ด๋ถ(= ๋จ์ผ ํ๋ก์ธ์ค ๋ด)์์๋ง ์ ํจํ ๋ฝ์ด๋ค.
์ฆ, ๋ฉํฐ ํ๋ก์ธ์ค ํ๊ฒฝ์ด๋ ๋ถ์ฐ ์์คํ ์์๋ ๋์์ฑ ๋ณด์ฅ์ด ๋์ง ์๋๋ค.
@Transactional
fun updateQty(id: Long) {
synchronized(lock) {
// synchronized ๋ธ๋ก ์์ ์ฝ๋๋ ํ๋์ ์ค๋ ๋๋ง ์ ๊ทผ ๊ฐ๋ฅ
...
}
}
์ฌ๊ณ ์๋ ๊ฐ์ ๋ก์ง์์ ์ฌ์ฉํ ์ ์๋ ๋ํ์ ์ธ ๋์์ฑ ์ ์ด ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ๋ค
์กฐํ์ Lock ์ ๊ฑธ๊ณ ํ ์คํธ.
@Repository
interface InventoryDetailRepository :
JpaRepository<InventoryDetail, Long>,
InventoryDetailRepositoryDSL {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM InventoryDetail i WHERE i.id = :id")
fun findByIdWithLock(id: Long): Optional<InventoryDetail>
}
๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์ ์ ์ฉ๋ ๊ฒ ๊ฐ๋ค.
select
...
id1_0.updated_at
from
inventory_detail id1_0
where
(
id1_0.deleted_at IS NULL
)
and id1_0.id=? for update
ํ์ง๋ง ๋ฝ์ ํ๋ํ ๋๊น์ง ํธ๋์ญ์
์ด ๋๊ธฐํ๊ธฐ ๋๋ฌธ์,
์ฑ๋ฅ์ ๋ณ๋ชฉ์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋๊ณ ์ผ๋ฐ์ ์ผ๋ก ์ถ์ฒ๋์ง๋ ์๋๋ค.
(์ถฉ๋์ด ๋ง์ด ๋ฐ์ํ๋ DB ํ
์ด๋ธ์ ํํด์๋ ๊ถ์ฅํ๋ค๊ณ ํ๋ค)
@Version
์ถ๊ฐ
@Entity
@Table(
name = "inventory_detail",
)
class InventoryDetail : AutoIncrementIdEntity() {
...
@Version
var version: Long? = null
}
๊ทธ๋ฆฌ๊ณ ๊ธฐ์กด DB ํ ์ด๋ธ์ ๋ฒ์ ์ 0์ผ๋ก ์ด๊ธฐ๊ฐ ์ธํ ํ๊ณ ํ๋ฒ ์์ฒญํด๋ณด์.
ํ์ธํด๋ณด๋ ์ถฉ๋์ฒ๋ฆฌ์ ๋ํ ๋ถ๋ถ์ ๋ฐ๋ก ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด ObjectOptimisticLockingFailureException
์ด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค๊ณ ํ๋ค
2024-09-01T21:46:41.349+09:00 ERROR 50605 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.~~.InventoryDetail#1]] with root cause
์ดํ ์ข ๋ ๋ฐฉ์์ ์ฐพ์๋ณด๊ณ ์ฌ์๋ ๋ก์ง์ ์ถ๊ฐํ๋ค
@Lock(LockModeType.OPTIMISTIC)
@Query("SELECT i FROM InventoryDetail i WHERE i.id = :id")
fun findByIdWithLock(id: Long): Optional<InventoryDetail>
@Retryable(
value = [Exception::class],
maxAttempts = 50,
backoff = Backoff(delay = 1000),
)
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun updateQty(id: Long) {
try {
val inventoryDetail =
inventoryDetailRepository.findByIdWithLock(id).orElseThrow {
throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
}
inventoryDetail.updateQty(inventoryDetail.getQty() - 1)
inventoryDetailRepository.saveAndFlush(inventoryDetail)
} catch (e: ObjectOptimisticLockingFailureException) {
log.error("Optimistic locking ์ถฉ๋ ๋ฐ์ !! : ${e.message}")
throw e
} catch (e: PersistenceException) {
log.error("PersistenceException ๋ฐ์: ${e.message}")
throw RuntimeException("PersistenceException ๋ฐ์", e)
} catch (e: Exception) {
log.error("๊ธฐํ ์์ธ ๋ฐ์: ${e.message}", e)
throw e
}
}
ํ์ง๋ง ์ค์ ํ
์คํธ์์๋ StaleObjectStateException ์์ธ๊ฐ ๊ณ์ ๋ฐ์ํ๊ณ ,
์์ธ๊ฐ ํธ๋์ญ์
๊ฒฝ๊ณ ๋ฐ์์ ๋ฐ์ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ catch๋ก ํฌ์ฐฉ๋์ง ์์๋ค.
โ Spring Retry, ์์ธ ํธ๋ค๋ง ๋ฑ์ ๋ณต์กํ๊ฒ ๊ตฌ์ฑํ์ง๋ง ๊ฒฐ๊ตญ ์คํจ๋ก ๊ฒฐ๋ก ์ง์๋ค.
(https://developer.jboss.org/thread/131217)
(https://stackoverflow.com/questions/30236145/not-able-to-catch-org-hibernate-staleobjectstateexception)
MySQL์ GET_LOCK, RELEASE_LOCK์ ํ์ฉํ์ฌ
์ด๋ฆ ๊ธฐ๋ฐ์ผ๋ก ๋ฝ์ ํ๋ํ๊ณ ํด์ ํ๋ ๋ฐฉ์์ด๋ค.
์ด๋ฆ์ ๊ฐ์ง Lock ์ ํ๋ํ ํ ํด์ ํ ๋๊น์ง ๋ค๋ฅธ ์ธ์
์ ์ด Lock ์ ํ๋ํ ์ ์์
Pessimistic Lock
๊ณผ ๋น์ทํ์ง๋ง Pessimistic Lock ์ ํ
์ด๋ธ์ Row, Table ๋จ์๋ก Lock ์ ๊ฑฐ๋ ๊ฒ์ด๋ฉฐ Named Lock
์ metadata ์ Lock ์ ๊ฑฐ๋ ๋ฐฉ๋ฒ ์ฆ, ๊ณต์ ์์ (Name) ์ ๋ํ Lock ์ ๊ฑฐ๋ ๊ฒ ์ด ๋ฐฉ์์ ๋ฐ์ดํฐ ์์ค๋ฅผ ์๋ก ๋ค๋ฅธ ๊ฒ์ผ๋ก ์ฌ์ฉํ๋๊ฒ ์ข๋ค๊ณ ์ด์ผ๊ธฐํ๋ค
์ด์ ๋ ์ปค๋ฅ์
ํ์ด ๋ถ์กฑํด์ง๋ ์ด์๊ฐ ์๊ธด๋ค
๋๊ฐ์ ์๋น์ค๋ก ๊ตฌํ
(ํธ๋์ญ์
์ ๊ฒฝ๊ณ์ ๋ฝ์ ์๋ช
์ฃผ๊ธฐ๋ฅผ ๋ช
ํํ ๊ด๋ฆฌํ๊ธฐ ์ํจ)
๋ถ๋ชจ-์์ ๊ตฌ์กฐ๋ก ๊ตฌํ
interface InventoryDetailRepository :
JpaRepository<InventoryDetail, Long>,
InventoryDetailRepositoryDSL {
@Query(
value = "select get_lock(:key, 3000)",
nativeQuery = true,
)
fun getLock(key: String)
@Query(
value = "select release_lock(:key)",
nativeQuery = true,
)
fun releaseLock(key: String)
}
@Service
class InventoryWithNamedLockService(
private val inventoryDetailRepository: InventoryDetailRepository,
private val inventoryUpdater: InventoryUpdater,
) {
private val log = logger()
@Transactional
fun updateQty(id: Long) {
try {
inventoryDetailRepository.getLock(id.toString())
inventoryUpdater.updateQty(id)
} finally {
inventoryDetailRepository.releaseLock(id.toString())
}
}
}
@Service
class InventoryUpdater(
private val inventoryDetailRepository: InventoryDetailRepository,
) {
private val log = logger()
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun updateQty(id: Long) {
val inventoryDetail =
inventoryDetailRepository.findById(id).orElseThrow {
throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
}
inventoryDetail.updateQty(
inventoryDetail.getQty() - 1,
)
log.info("qty : {}", inventoryDetail.getQty())
}
}
๋์์ฑ ๋ฌธ์ ํด๊ฒฐ. ์์ ์ ์ผ๋ก ๋์.
๊ทธ๋ฌ๋, ํธ๋์ญ์
์ข
๋ฃ ์ ๋ฝ์ด ์๋ ํด์ ๋์ง ์์ โ ์ง์ ํด์ ํ์
๋ํ์ ์ผ๋ก ๋ ๊ฐ์ง ๋ฐฉ์์ด ์๋ค๊ณ ํ๋ค
์ด๊ฒ๋ Named Lock
๊ณผ ๋น์ทํ ๋ฐฉ์์ผ๋ก ๊ตฌํ๋๋ค ๋จ์ง Redis ๋ฅผ ํ์ฉํ ๋ฟ.
์ผ๋จ Redis setnx
๋ช
๋ น์ด๋ฅผ ํตํด key ์ value ๋ฅผ ์ค์ ํด์ฃผ๋ RedisRepository ๋ฅผ ๊ตฌํํ์
@Component
class RedisLockRepository(
private val redisTemplate: RedisTemplate<String, String>,
) {
fun lock(key: Long): Boolean? =
redisTemplate
.opsForValue()
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3000))
fun unlock(key: Long): Boolean = redisTemplate.delete(generateKey(key))
fun generateKey(key: Long) = key.toString()
}
์ดํ ๋ถ๋ชจ-์์ ๊ตฌ์กฐ๋ฅผ ํตํด
๋ถ๋ชจ ๋ ์ด์ด๋ Redis ์ key ์ ๋ํ Lock ํด์ ๋ฐ ํ๋์ ๊ตฌํ (spin lock)
์์ ๋ ์ด์ด๋ ๊ธฐ์กด ์ฌ๊ณ ์๋ ์ฒ๋ฆฌ ๋ก์ง
@Service
class InventoryLettuceLockService(
private val redisLockRepository: RedisLockRepository,
private val inventoryUpdater: InventoryUpdater,
) {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun updateQty(id: Long) {
while (!redisLockRepository.lock(id)!!) {
Thread.sleep(100)
}
try {
inventoryUpdater.updateQty(id)
} finally {
redisLockRepository.unlock(id)
}
}
}
์ดํ Redis io ๊ด๋ จ ๋ก๊ทธ
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils : Fetching Redis Connection from RedisConnectionFactory
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] io.lettuce.core.RedisChannelHandler : dispatching command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1] write() done
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] write(ctx, AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandEncoder : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379] writing command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Received: 4 bytes, 1 commands in the stack
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Stack contains: 1 commands
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.l.core.protocol.RedisStateMachine : Decode done, empty stack: true
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler : [channel=0x26ca5e1a, /127.0.0.1:50710 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Completing command AsyncCommand [type=DEL, output=IntegerOutput [output=1, error='null'], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils : Closing Redis Connection
๊ทธ์น๋ง ํด๋น ๋ฐฉ์์ ์์ํ๋ฏ spin lock ๋ฐฉ์์ด๋ฏ๋ก redis ์ ๋ถํ๋ฅผ ์ค ์ ์์
์ถ์ฒํ์ง ์์.
๊ธฐ์กด Observer ํจํด๊ณผ ๋น์ทํ๊ฒ ์ฑ๋์ ๊ตฌ๋
ํ ์ดํ ๋ค๋ฅธ ์ธ์
์ด Lock ์ ํด์ ํ ๊ฒฝ์ฐ ๊ด๋ จ ์ด๋ฒคํธ์ ๋ํ ์๋ฆผ์ ์ฃผ๊ณ ,
๊ตฌ๋
ํ ๋ค๋ฅธ ์ธ์
์ด ํด๋น ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ Lock ์ ํ๋ํ๋ ๋ฐฉ์
Redis ๋ช ๋ น์ด๋ก ์์๋ณด์.
๊ตฌ๋
127.0.0.1:6379> subscribe ch1
1) "subscribe"
2) "ch1"
3) (integer) 1
publish
127.0.0.1:6379> publish ch1 hello
(integer) 1
127.0.0.1:6379>
๊ตฌ๋
1) "message"
2) "ch1"
3) "hello"
pub_sub ๊ธฐ๋ฐ์ด๋ฏ๋ก Lettuce ๋ณด๋ค Redis ๋ถํ๊ฐ ์ค์ด๋ ๋ค, Redisson ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ด๋ฏธ Lock ํ๋ ๋ฐ ํด์ ๊ฐ ๊ตฌํ๋์ด ์์ผ๋ฏ๋ก ๋ช ๋ น์ด๋ฅผ ์ฌ์ฉํ๋ RedisRepository ๋ ํ์ ์๋ค
@Service
class InventoryRedissonLockService(
private val redissonClient: RedissonClient,
private val inventoryUpdater: InventoryUpdater,
) {
private val log = logger()
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun updateQty(id: Long) {
val lock = redissonClient.getLock(id.toString())
try {
val enabled = lock.tryLock(10, 1, TimeUnit.SECONDS)
if (!enabled) {
log.info("Redis Lock ํ๋ ์คํจ Key : {}", id)
return
}
inventoryUpdater.updateQty(id)
} catch (e: InterruptedException) {
throw RuntimeException(e)
} finally {
lock.unlock()
}
}
}
๋น๊ต์ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ๊ด๋ จ๋ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ์์ด์ ๊ฐ๋จํ๋ค.
ํ
์คํธ ๊ฒฐ๊ณผ๋ ์ ์ ์๋ํ๋ฉฐ ๊ตฌํ ๋ํ ๊ฐ๋จํ๋ค
๊ทธ๋ฌ๋ ๋ฝ ํด์ ์คํจ ๋ฌธ์ , ๋ฝ์ด ํ๋ํ์ง ๋ชปํ์ ๊ฒฝ์ฐ ์๋ ํด์ ์๊ฐ ์ค์ ๋ฑ ์ฌ๋ฌ๊ฐ์ง ์์ธ๋ค์ ์๊ฐํด์ผ ํ๋ค ๊ทธ๋ฆฌ๊ณ Redis ๋ผ๋ ์ธ๋ถ ์์์ ํ์ฉํ๋ ๋งํผ ์์ํ์ง ๋ชปํ๋ ์ด์๋ค์ด ์๊ฒจ๋ ์๋ ์๋ค
์ด์ธ์๋ ๋ฉ์์ง ํ๋ฅผ ํ์ฉํ ๋์์ฑ ์ ์ด ๋ฑ์ด ์กด์ฌํ๋ค
์ถ๊ฐ๋ก K6
๋ฅผ ํ์ฉํ๋ฉด์ ๋์์ฑ ํ
์คํธ๋ฅผ ์งํํ๋๋ฐ ์ข ์ฐพ์๋ณด๋ TPS (Transaction Per Seconds) ์ ๋ํ ํ
์คํธ๋ ๊ฐ๋ฅํด ๋ณด์ธ๋ค
๊ฐ๋จํ ์ฌํ๊น์ง์ ๋์์ฑ ํ ์คํธ ์คํฌ๋ฆฝํธ๋ฅผ ๋๋ ค์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด,
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
* simultaneous_requests: 1 iterations for each of 10 VUs (maxDuration: 10s, gracefulStop: 30s)
data_received..................: 1.6 kB 1.3 kB/s
data_sent......................: 1.2 kB 934 B/s
http_req_blocked...............: avg=1.87ms min=1.82ms med=1.85ms max=2.02ms p(90)=1.92ms p(95)=1.97ms
http_req_connecting............: avg=618.8ยตs min=553ยตs med=615.49ยตs max=704ยตs p(90)=675.2ยตs p(95)=689.6ยตs
http_req_duration..............: avg=766.22ms min=292.99ms med=761.23ms max=1.23s p(90)=1.14s p(95)=1.18s
{ expected_response:true }...: avg=766.22ms min=292.99ms med=761.23ms max=1.23s p(90)=1.14s p(95)=1.18s
http_req_failed................: 0.00% โ 0 โ 10
http_req_receiving.............: avg=77.89ยตs min=59ยตs med=68.5ยตs max=131ยตs p(90)=110.3ยตs p(95)=120.65ยตs
http_req_sending...............: avg=293.3ยตs min=177ยตs med=316ยตs max=366ยตs p(90)=346.19ยตs p(95)=356.1ยตs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=765.85ms min=292.64ms med=760.96ms max=1.23s p(90)=1.14s p(95)=1.18s
http_reqs......................: 10 8.050875/s
iteration_duration.............: avg=770.42ms min=297.84ms med=765.39ms max=1.24s p(90)=1.14s p(95)=1.19s
iterations.....................: 10 8.050875/s
vus............................: 3 min=3 max=3
vus_max........................: 10 min=10 max=10
์ฌ๊ธฐ์ http_reqs
๋ฅผ ๋ณด๋ฉด 10๊ฐ์ ์์ฒญ์ด ๋ค์ด๊ฐ๊ณ 1์ด์ 8๋ฒ์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๋ค๊ณ ๋ณผ ์ ์๋ค