Golang, MongoDB client connection pool 관련

최근 중국 커머스 관련 최대 이벤트인 광군제(11/11)를 대비하여 성능 테스트를 진행하고 있습니다. 일년 전체 매출의 절반 이상이 이 행사 기간 중에 나오기 때문에 트래픽도 평소 대비 몇 십배 이상 나옵니다. 다시 말하면 평소 운영되는 서비스는 그냥 기능 테스트 정도 수준이고 모든 서비스 관련 사항들이 이날에 준비되어야 한다는 것입니다. 일년에 하루만 일하고, 이날만 잘 돌아가면 모든게 무사 통과라고도 합니다.

MongoDB  CPU 이상 사용

운영 환경에 성능 테스트를 수행할 수 없어 HUAWEI 클라우드에 별도의 테스트 클러스터를 구성하여 테스트를 수행하였습니다. 이중 SQLServer, MongoDB와 같은 것은 운영 환경은 RDS를 사용하고 있지만 테스트 환경에서는 RDS를 사용하기 어려워 가상 서버에 직접 설치하여 테스트를 수행하였습니다.

성능 테스트 중에 다음과 같은 부하 상황에서 MongoDB가 CPU 1개만 사용하는 것이 확인 되었습니다.

golang_mongo_1

MongoDB를 사용한 것은 일년 가까이 되어 가는데 지금 발견한 것입니다. 이렇게 CPU 사용 이상을 늦게 발견한 것은 RDS  서비스를 사용하게 되면 해당 서버에 콘솔로 접속하기는 어렵고 RDS 서비스에서 제공하는 웹 기반 모니터링 화면에서 제공하는 정보만 확인하였는데 여기서는 전체 CPU 사용 현황만 나타나기 때문에 항상 CPU 사용이 낮게 나와서 의심을 하지 않았습니다. CPU가 4개라면 1개 100% 사용하면 실제 CPU 사용은 25%로 나타나기 때문입니다. 물론 mongo에서 제공하는 도구를 사용할 수도 있지만 RDS 서비스의 웹 콘솔에 나타나는 숫자만 보고 별 의심이 없었던 것입니다.

MongoDB는 Write시 Sequence하게 처리한다?

google에서 검색해서 "mongodb how many cpu"로 검색해보니 다음과 같은 문서들이 나타납니다.

mongodb_cpu

이 중 "https://stackoverflow.com/questions/4407336/mongodb-utilizing-multi-cpu-server-for-a-write-heavy-application" 링크에 있는 내용을 보면 CPU를 제대로 활용하지 못하고 해결책으로는 Shard를 사용하라고 되어 있습니다. 다른 여러 문서도 동일합니다. 위 스택플로우 내용 중 댓글에 보면 다음과 같은 내용이 있습니다.

Has this answer changed in the past two years? – Philipp Mar 10 '13 at 23:27 @Philipp the question has changed in two years. This question states as assumptions things that are no longer true. Therefore it's a bad idea to add bounty to a misleading (as of today) question. Please ask a new question if you want to know how to maximize write-throughput. There are very few applications which are actually limited by the write lock - most encounter I/O bandwidth limitations far before then. – Asya Kamsky Mar 11 '13 at 23:34 @Philipp under the most recent version, the write lock is now implemented at the DB level. Of course, in theory this would mean that all non-trivial collections (tables) should now be implemented as independent databases to maximize throughput. In practice, I do not know how well this will work and strongly recommend testing your use case. I have regularly seen maxed out Disk IO without any "tricks". – Gates VP Mar 12 '13 at 0:25

내용은 최근 버전에서는 문제가 해결되었으니 다시 확인해보라는 내용입니다. Google의 검색 결과에 나오는 global lock에 대한 문서들도 보면 대부분 오래된 문서들입니다. 조금 더 찾아보니 다음 문서에서 MongoDB 3.0 이상에서는 Document level lock을 사용하고 있어서 Multi CPU 환경에 잘 대응한다고 되어 있습니다.

WiredTiger is a new storage engine for MongoDB, developed by the architects of Berkeley DB, the most widely deployed embedded data management software in the world. WiredTiger scales on modern, multi-CPU architectures. Using a variety of programming techniques such as hazard pointers, lock-free algorithms, fast latching and message passing, WiredTiger performs more work per CPU core than alternative engines

Golang의 MongoDB 클라이언트 의심

제가 주로 사용했던 MongoDB 클라이언트가 Java 이었는데 그 당시에는 이런 문제를 겪지 않았다는 생각에 지금 서비스에서 사용하고 있는 Golang의 MongoDB 클라이언트 라이브러리에 의심이 갔습니다. Golang MongoDB 클라이언트는 mgo를 사용하고 있습니다. 이 테스트를 수행하기 전에는 한번도 golang -> mongo를 사용하는 코드를 만들지도 않았고, 만들어진 코드를 본 적도 없기 때문에 mgo의 Document를 유심히 보았습니다. 메인 페이지에 다음과 같은 내용이 있는데 이것이 단서가 되었습니다.

Synchronous and concurrent mgo offers a synchronous interface, with a concurrent backend. Concurrent operations on the same socket do not wait for the previous operation's roundtrip before being delivered. Documents may also start being processed as soon as the first document is received from the network, and will continue being received in the background so that the connection is unblocked for further requests.

기본적으로 Connection pool 기능을 제공한다고 하면서 내용에 보면 "Same socket" 이라는 표현이 나타나는데 이 내용만으로 추측해보면 하나의 socket을 열어서 사용하는 듯한 느낌이었습니다. select 인 경우야 하나의 socket으로도 잘 처리할 수도 있겠지만 insert의 경우는 문제가 됩니다. 다시 다른 문서를 보니 다음과 같은 예제 코드가 눈에 띄었습니다.

그리고 저희 쪽에서 사용하는 코드를 보니 다음과 같이 되어 있었습니다.

1
2
3
4
session := m.Clone()
defer session.Close()
c := session.DB(m.databaseName).C(collection)
return s(c)

mongoSession의 Clone()을 사용하고 있습니다. Clone()과 Copy()의 함수 설명을 보면 다음과 같이 되어 있습니다.

First of all, we need to see the difference between mgo.Session.Copy() and mgo.Session.Clone(). While go.Session.Clone() returns a new session, the session uses the same socket connection. That isn't necessarily a bad thing, but keep in mind that on the server side, a stack is allocated per connection. So the sessions would share the same stack. Depending on your use cases, that may make a big difference. And here is the problem – if you open a new socket connect for each record, this leads to a three way handshake, which is slowish. Reusing the same socket reduces this overhead, but there still is some and has the drawback described above.

Session의 Clone() 함수는 같은 Socket connection을 사용하는 세션을 생성하고, Copy()는 매번 새로운 Socket 생성하기 때문에 TCP/IP 연결 부하 등이 있다는 내용입니다.

Copy() 사용한 후 다시 테스트

Clone() 함수를 사용할 경우 CPU를 하나만 사용하는 문제가 있으니 Copy() 함수를 사용하도록 코드를 수정하여 다시 테스트를 수행했습니다. 다음 화면은 테스트 수행 결과입니다. 보시는 것처럼 이제는 모든 CPU를 사용하고 있는 것을 볼 수 있습니다. MongoDB 프로세스가 390% 정도 사용하고 있습니다.

golang_mongo_2

Copy() 사용한다고 해서 모든 문제가 해결되지는 않을 것 같습니다. Copy() 함수 설명에서도 나와 있듯이 Copy() 메소드를 실행할 때 마다 TCP connection을 위한 작업과 MongoDB의 인증 작업 등을 모두 수행하기 때문에 모든 Request에 대해 이렇게 수행하는 것도 성능적으로 좋지 않기 때문입니다. 일반적인 Connection Pool 에서는 미리 이런 작업까지 수행이 끝난 온전히 Connection을 유지하고 있는 Connection Object를 유지하고 있고 이를 프로그램에서 사용하고 반환하는 방식으로 구현되어 있습니다. 이런 부분은 현재 mgo 라이브러리에는 구현되어 있지 않은 것 같습니다. 제가 기대 했던 Connection Pool은 이런 방식의 동작을 기대했는데 이 부분은 별도로 구현을 해야 할 것 같습니다.

이렇게 보니 이 문제 해결을 위해 검색해본 문서 중 다음 표현이 눈에 많이 거슬리네요.

결론

처음 이글을 쓸때에는 단순히 Clone() 사용해서 문제가 있어 Copy()를 사용해서 해결했다 이렇게만 쓰려고 하다가 문제 해결 과정을 작성하는 방향으로 글의 흐름을 잡았습니다. 이유는 여기 북경에 와서 많이 접하는 어려움이 여러 개발자들이 문제를 정의하고 풀어나가는 방법에 익숙하지 않아 문제 해결을 못하고 있는 것이 었습니다. 이런 문제 해결 방식에 익숙하지 않은 개발자는 모든 조직에 있게 되는데 이런 글을 통해서라도 문제 해결 방법에 조금씩 익숙해졌으면 하는 바램으로 이런 전개를 선택했습니다.

  1. 문제를 정의한다.

    여기는 왜 MongoDB가 CPU 하나만 사용하나?

  2. 가설을 세운다.

    첫번째 가설은 MongoDB 자체의 write lock 문제

  3. 가설이 맞는지 확인한다.

    이 과정도 잘못된 정보를 가지고 잘못 분석할 수도 있다는 것을 알 수 있음.

  4. 다른 가설을 세운다.

    Client의 문제가 아닐까?

  5. Client 라이브러리 자체 소스 코드 등 확인

    해결 방법 선택

  6. 선택된 해결 방법으로 다시 실험

    문제 해결 확인


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.