회사에서 클라우드로 서비스를 옮겼습니다. 그런데 메모리 누수가 발생해서 분석해보니 Databuffer 해제를 해주지 않아서 발생한 문제였습니다. 도대체 databuffer가 무엇이기 때문에 이러한 문제가 발생했는지 알기 위해서 정리했습니다. Databuffer를 알기 위해서 Buffer부터 살펴보기로 했습니다.
Buffer
버퍼는 두 장치간에 속도차이로 인한 처리 속도 지연을 방지하기 위해서 도입된 개념입니다. 아래 예시를 보겠습니다. CPU는 100개의 작업을 처리할 수 있지만, DISK는 1개를 처리할 수 있습니다. DISK가 1개를 처리하는 동안에는 CPU의 프로세스가 잡혀 있기 때문에 CPU도 1개를 처리할 때까지 기다려야 합니다. 그렇다면, CPU는 1/100를 효율을 발휘하고 있습니다.
이때, 가운데에 CPU의 빠른 작업 받아서 보관해 주는 무엇인가가 있으면 어떨까요? CPU는 바로 DISK에게 작업을 줄 필요이 자신의 일을 빨리 처리할 수 있습니다. 이렇게 가운데서 받아주는 것을 RAM(메모리)라고 합니다. 그리고 RAM에는 버퍼 영역이 있습니다. (cpu 연산에서 버퍼는 대게 메모리 내의 버퍼 영역 입니다. 그 외에도 다양한 장치에는 메모리 같은 버퍼 영역이 있습니다)
Cache
버퍼와 비슷한 캐시라는 녀석도 있습니다. 버퍼와 마찬가지로 두 장치간의 속도차이로 인한 처리 지연을 처리하기 위해서 도입된 개념입니다. 버퍼와의 다른 점은 속도가 빠른 장치의 관점에서 문제를 해결했다는 것입니다. 디스크에서 가져온 데이터를 캐시에 저장하고 cpu가 빠르게 가져다가 사용할 수 있게 합니다. 반대로 버퍼는 속도가 느린 장치의 관점에서 해결했습니다.
spring core에서 제공하는 databuffer는 자바의 bytebuffer와 거의 같습니다. 그러면 bytebuffer는 무엇인지 간략하게 살펴보겠습니다.
ByteBuffer
바이트버퍼는 바이트를 저장하고 읽는 저장소입니다. 그렇기 때문에 읽기와 쓰기를 당연히 지원하고 있습니다. 자바에서 바이트버퍼를 사용하는 이유는 NIO를 사용하기 위해서 입니다. 자바가 C나 C++에 비해서 느린 이유는 IO가 JVM 내부에 있기 때문입니다. 그래서 NIO를 사용하여 속도를 높이려고 합니다. 보통 TCP/IP 통신이나 DB와의 통신을 할 때 주로 사용합니다.
allocate()라는 메서드를 사용하면 JVM 힙 영역에 버퍼를 할당하고, allocateDirect() 메서드를 사용하면 OS 커널 영역에 버퍼를 할당합니다. OS 커널 영역에 버퍼를 두고 사용할 때의 이점은 OS가 IO를 하기 위한 시간이 줄기 때문입니다.
이제 databuffer를 살펴보겠습니다. 그리고 databuffer 사용을 도와주는 다른 것들도 함께 보겠습니다.
Databuffer
데이터버퍼는 위에서 설명한 바이트버퍼와 유사하지만 몇 가지 이점이 있습니다. 읽기와 쓰기를 독립적인 위치에서 하기 때문에 flip()이라는 메서드를 사용할 필요가 없습니다. 그리고 용량은 StringBuilder를 사용하여 필요에 따라 확장이 가능합니다.
netty에서 사용할 때에는 메모리 풀(memory pool)을 사용합니다. 그래서 데이터버퍼를 해제해 주지 않는다면 메모리 누수가 발생할 수 있습니다. pooled된 데이터버퍼를 사용할 때에는 reference count(이하 refCnt)라는 개념을 알아야 합니다. refCnt는 수동으로 올리 수 있습니다. 사용하면 감소하게 되고 0이면되 해제됩니다. 따라서 데이터버퍼 사용시에 refCnt가 0이 되어 버퍼를 해제할 수 있도록 주의를 기울여야 합니다.
OS 커널 영역에 버퍼를 생성하는 다이렉트 버퍼(direct buffer)를 사용합니다. 따라서 IO 성능이 좋습니다.
DataBufferFactory
데이터버퍼를 생성합니다. 생성하는 방법은 2가지가 있습니다. 첫 번째, 크기를 알고 있어서 새로운 데이터 버퍼를 만드는 것입니다. 두 번째, 기존에 있는 byte[]를 wrap() 메서드를 감싸거나 java.nio.bytebuffer로 감싸는 방법입니다.
PooledDataBuffer
데이터버퍼의 확장된 개념으로 reference count 개념이 있습니다. 처음 pooledDataBuffer가 할당되면 refCnt = 1입니다. retain() 메서드를 통해서 refCnt를 올려줄 수 있고, release()를 통해서 refCnt를 내려줄 수 있습니다. refCnt가 0보다 높으면 버퍼가 해제되지 않기 때문에 다 사용한 이후에는 꼭! 0으로 만들어서 해제해 주어야 합니다.
DataBufferUtils
데이터버퍼에서 동작할 수 있는 유용한 몇 가지 기능을 제공합니다. 첫 번째로, 두 개의 데이터버퍼 스트림을 copy없이 하나의 버퍼로 합칠 수 있는 기능이 있습니다. 두 번째로, 데이터버퍼가 pooledDataBuffer의 인스턴스라면 retain()과 release()를 제공합니다. 그외 Flux로 바꿔주는 등 기능을 제공합니다.
이렇게 DataBuffer에 대한 내용을 간략하게 알아보았습니다. 더 궁금하거나 자세한 내용은 Spring Docs에서 확인하실 수 있습니다.
끝.
'IT > Spring' 카테고리의 다른 글
[리액티브 API] WebClient 개요와 사용법 (0) | 2022.07.26 |
---|---|
[리액티브 API] 비동기 웹 프레임워크, WebFlux (0) | 2022.07.26 |
[리액터] 리액터 사용하기 (0) | 2022.07.11 |
[리액터] 리액터 개요 (0) | 2022.07.11 |
Spring boot와 MariaDB를 JPA로 연동 (0) | 2022.03.20 |