지난 업무을 회고하면서 커리어에 의미있는 경험들을 글로 정리하고자 하였다.
그 중 OpenStack 기반의 퍼블릭 클라우드를 구축하고 운영했던 업무에 대해 먼저 적어본다.
그때가 2016년, 지금은 거의 관심을 받지 못하지만 당시에는 OpenStack 프로젝트가 꽤 주목받던 때였다.
당시 일하던 회사에서는 퍼블릭 클라우드 사업을 하고 있었고 여러 OpenStack 파생 프로젝트 중, Mirantis OpenStack 7.0 를 기반으로 해서 새로운 클라우드 플랫폼을 만들고 있었다.
Mirantis OpenStack(이하 MOS) 의 구조는 크게는 Kubernetes 의 구조와 거의 동일하다.
Controller Nodes = Control Plane
Compute Nodes = Data Plane
Ceph Nodes = 스토리지 백엔드
Controller Nodes 를 좀 더 살펴보면,
Compute Nodes 는 간단한데,
큰 그림에서 MOS 는 위와 같은 구조로 여기서 나는 블록 스토리지 파트를 담당했다.
블록 스토리지는 OpenStack 의 여러 컴포넌트 중 cinder 가 책임지는 부분이다.
cinder 로 볼륨 생성 요청이 전달되면 cinder 는 자신에게 연결된 스토리지 백엔드에서 실제 I/O를 처리할 볼륨을 생성하고 해당 볼륨 정보를 기반으로 cinder 볼륨을 생성한다.
볼륨 기능을 구현하기 위해서는 먼저 스토리지 백엔드를 선정해야 했다.
위 그림에서는 Ceph 를 백엔드로 사용하지만 회사에서는 상용 스토리지를 사용하는 방향으로 진행했고 여러 벤더 중 당시 가장 OpenStack 과 호환성이 좋고 가성비가 좋았던 NetApp 의 FAS 시리즈 스토리지를 벡엔드로 선정하였다.
그리고 하이엔드 볼륨 용으로 SolidFire 스토리지가 백엔드로 (NetApp FAS 스토리지보다 먼저) 선정되었다.
벡엔드 선정 후 한 일은 PoC 이다. PoC 에서 중점적으로 본 부분은,
이렇게 세가지인데 특히 성능 측정에 많은 시간을 들였고 성능 측정 툴인 fio 를 매우 다채롭게(?) 사용하였다.
성능 측정의 의도는 최대 IOPS 와 Throughput 이 얼마나 나오는가 를 확인하는 것이다.
이를 위해 다양한 케이스를 만들어 I/O 스트레스를 발생시키는 작업을 반복하는데, 테스트 초기에 Read I/O 측정에서 문제가 있었다.
우리는 디스크를 분산하여 많이 사용하면 할 수록 당연히 읽기 성능도 높이 나올거라 예상했는데 몇 개의 디스크를 사용하는지와는 상관없이 성능이 매번 잘나왔다.
원인 파악을 위해 I/O 발생 내부를 분석해보니, I/O 가 처리 될 때 OS 차원에서 메모리에 캐싱(cache)하며 I/O 가 진행되는 걸 인지 못하고 그냥 스트레스만 무작정 생성했기 때문에 이런 잘못된 결과가 나타난 것이었다.
fio 를 통해 생성된 I/O 가 스토리지 디스크까지 가지 못하고 OS 레벨 메모리 캐시에서 처리되었고 그렇기 때문에 디스크 갯수와 상관없이 성능이 잘 나왔던 것이다.
이를 해결하기 위해선 fio 옵션에 direct=1 를 넣어주었고 이후 테스트부턴 바로 스토리지로 I/O 를 보낼 수 있었다.
하지만 하나 더 문제가 있었으니, 이는 넷엡 스토리지 레벨에서의 캐싱이었다.
I/O 가 스토리지로 갔지만 넷앱 스토리지도 캐싱을 수행하여 디스크까지 안가고 스토리지 메모리에서 처리가 끝나는 것이었다.
이를 바꿀 옵션은 당연히 fio 에 없고 스토리지에서도 제공하지 않았다.
결국 최종적으로 사용한 방법은 매번 fio 테스트 마다 읽기 I/O 생성에 사용되는 더미 파일을 새로 만들며 진행하는 것이었다. 사용된적이 없는 새 파일이라면 캐싱되어 있을게 없으니까.
이외에도 fio 의 다양한 옵션들을 사용하였는데 대표적으로 아래와 같다
numjobs=int: 병렬로 다수의 I/O 발생시켜 스트레스 더 효과적으로 발생시킨다. group_reporting 옵션과 함께 사용하면 좋다.rw=[read|write|randread|randwrite|rw|randrw]: I/O 패턴을 선택한다. 주로 randrw 사용했다.rwmixread=int rwmixwrite=int: Read/Write 비율 설정한다. 실제 운영 R/W 비율을 적용하여 PoC 의 신뢰성을 높일 수 있다.runtime=int: 여기서 정해진 시간 만큼만 I/O 를 발생시킨다. time_based=int 과 함께 사용하면 좋다.PoC 가 완료된 이 후부터 본격적으로 cinder 와 연동 작업을 시작했다.
이때 부터 cinder 기능이 안정적으로 작동하는지 특히 드라이버의 적합성을 중점적으로 확인하였는데, kilo 버전의 넷엡 드라이버는 여러가지로 이슈가 많이 나타났다.
특히 가장 큰 이슈 중 하나는 cinder 볼륨을 삭제하면 해당하는 매핑된 LUN 만 삭제해야 하는데 상관없는 다른 LUN 도 함께 지워지는 문제였다. 해당 버그는 드라이버 코드 이슈로 매핑된 볼륨을 정확히 구분하는 메써드가 제대로 구현안되서 나타나는 문제였고 직접 드라이버를 수정하여 해결할 수 있었다.
liberty 부터는 드라이버가 전체적으로 다 바뀌어 해당 이슈가 아마도 더 이상 발생하지 않을듯 하다.
대략 위와 같은 작업을 수행하며 cinder 서비스 구현을 완성했고 다른 팀원들의 작업도 함께 더해 퍼블릭 클라우드 서비스를 1차 완성할 수 있었다.
하지만 서비스라는 것은 구축 완료부터 시작이니 이제부터 본격적인 서비스 운영 업무에 들어갔고 이때부터 구축때와는 비교할 수 없는 더 큰 이슈들이 펑펑 터졌다.
다음 글에서는 서비스 운영하며 느낀 점들에 대해서 이어서 적어보려 한다.