월별 글 목록: 2013년 12월월

MongoDB를 쓰면서 알게 된 것들

최근 이음의 글로벌 프로젝트 Hey에서는 많은 수의 유저들에 대한 매칭을 원활하게 처리하기 위해 가장 많이 쓰이고 있는 NoSQL 스토리지 중 하나인 MongoDB를 도입했습니다. RDBMS에 익숙했던 개발자들 입장에서는 굉장히 낯선 부분들이 많았지만 여러가지 시행 착오들을 겪으면서 어느 정도 안정화된 상태로 사용할 수 있게 되었습니다. 1년 전쯤에 개발자 커뮤니티에서 화제가 되었던 Things I wish I knew about MongoDB a year ago 라는 글이 있었습니다. 이번 글에서는 이음에서 MongoDB를 쓰면서 겪었던 여러가지 이야기들을 풀어내 봄으로써, MongoDB 엔진 도입을 검토하고 있는 개발자 분들, 더 넓게는 NoSQL 기술에 관심이 있는 개발자 분들과 다양한 의견들을 나눌 수 있었으면 합니다.

 

mongo-db-huge-logo

MongoDB가 뭔가요?

MongoDB는 10gen (최근 MongoDB로 사명이 변경되었습니다.) 에서 만든 document 기반의 NoSQL 스토리지입니다. 엔진은 C++로 작성되었으며, 오픈 소스입니다. 대표적인 특징으로는 다음과 같은 것들이 있습니다.

– Document-Oriented Storage : 모든 데이터가 JSON 형태로 저장되며 schema가 없습니다.
– Full Index Support : RDBMS에 뒤지지 않는 다양한 인덱싱을 제공합니다.
– Replication & High Availability : 데이터 복제를 통해 가용성을 향상시킬 수 있습니다.
– Auto-Sharding : Primary key를 기반으로 여러 서버에 데이터를 나누는 scale-out이 가능합니다.
– Querying : key 기반의 get, put 뿐만이 아니라 다양한 종류의 쿼리들을 제공합니다.
– Fast In-Place Updates : 고성능의 atomic operation을 지원합니다.
– Map/Reduce : 맵리듀스를 지원합니다.
– GridFS : 별도 스토리지 엔진을 통해 파일을 저장할 수 있습니다.

 

RDBMS로는 안되나요?

Hey에서는 모든 회원들을 상대로 하루 3개 이상의 매칭을 제공합니다. 하나의 매칭을 RDBMS에서 하나의 row로 생각하면, 하루에 회원 수 * 3개의 row가 추가되는 것이죠. (게다가 어른의 사정(?) 때문에 실제로는 더 많은 수가 추가되는 경우가 대부분입니다.) 이렇게 1년이 쌓이면 회원 수 * 1000개가 되겠죠? 유저가 10만이면 1억입니다. 이렇게 많은 숫자의 매칭을 관리하기에 RDBMS는 한계가 있다고 판단되어, scale-out이 가능한 MongoDB를 사용하게 된 것입니다. 사실 처음엔 Apache HBase를 사용하려고 헀으나, HBase는 다음과 같은 문제가 있었습니다.

– Rails 어댑터의 부재 : Hey의 API 서버는 Ruby on Rails를 사용하여 개발되었습니다. Rails에서 HBase를 사용하기 위한 gem인 massive_record가 있긴 했지만, 프로덕션에서 사용하기엔 갈 길이 멀어 보였습니다. (HBase 자체의 문제는 아닙니다.)

– Hadoop의 도입 비용 : 개발팀 내에 Hadoop 경험자가 거의 없어서 클러스터 관리가 어려웠고, Hadoop 자체가 기본적으로 많은 (2~3대 이상) 머신들을 필요로 합니다.

– 미약한 Index 기능 : HBase는 거의 key-value 스토리지처럼 작동하기 때문에 다양한 쿼리들을 실행하기 위해서는 데이터를 full scan해야만 하므로, 스토리지 전체의 성능을 저하시킵니다.

이런 이유때문에 Hey에서는 매칭 데이터 및 document형 데이터는 MongoDB에, 기타 관계형 데이터는 MySQL에 저장하도록 구현하였습니다.

서론이 길었네요 🙂 이제부터 본격적으로 시작해 보도록 하겠습니다.

 

1. Indexing이 자유롭습니다.

MongoDB에서는 가장 많이 쓰이는 RDBMS인 MySQL에서 지원하는 대부분의 인덱스를 지원합니다. 지원하는 인덱스의 목록은 대략 다음과 같습니다.

– Single Field Indexes : 가장 기본적인 인덱스 타입
– Compound Indexes : RDBMS에서 많이 쓰이는 복합 인덱스
– Multikey Indexes : Array에 매칭되는 값이 하나라도 있으면 인덱스에 추가하는 멀티키 인덱스
– Geospatial Indexes and Queries : 위치 기반 인덱스와 쿼리
– Text Indexes : String 컨텐츠에도 인덱싱이 가능
– Hashed Index : BTree 인덱스가 아닌 Hash 타입의 인덱스도 사용 가능

이처럼 강력한 인덱스 기능 덕분에 거의 모든 쿼리들을 빠르게 처리할 수 있습니다. 이는 MongoDB를 선택한 가장 큰 이유인 동시에, 다른 NoSQL들에서는 찾아보기 힘든 장점이기도 합니다.

 

2. 많은 데이터를 한꺼번에 insert하면 안됩니다.

Row 단위의 락을 지원하는 대부분의 RDBMS와는 달리 MongoDB에서는 데이터베이스 단위의 락을 사용합니다….. 뭐..뭐라구요?

e0020102_03085040

기존의 RDBMS 사용자들에게는 그야말로 충격과 공포죠.  하지만 여기서 끝이 아닙니다. MongoDB에서는 데이터베이스별로 Read Lock과 Write Lock이 있는데, read lock은 여러 개의 operation에서 공유가 가능하지만 write lock은 하나의 operation만이 사용할 수 있으며 (중간에 read operation도 허용하지 않습니다.) write lock은 항상 read lock보다 우선권을 갖습니다. 즉, read operation과 write operation이 동시에 queue에서 대기하고 있으면 항상 write operation이 실행된다는 것입니다. 따라서 MongoDB에서 대량의 데이터 (수만 이상) 를 한꺼번에 insert하면, DB가 insert의 수행때문에 다른 작업을 수행하지 못하게 됩니다. 장비를 정지합니다. 아..안되잖아?

따라서 지금 Hey에서는 대량의 insert 시에는 항상 수천 개 정도의 작은 단위로 쪼개어서 실행하고 있습니다. 그나마 MongoDB의 write가 메모리에 쓰는 방식이기 때문에 RDBMS에 비해 성능 저하는 덜하고, 긴 operation의 경우 lock을 양보하기도 하지만 (자세한 내용은 공식 문서를 참조) 그래도 찜찜한건 사실입니다.

 

3. 쿼리 문법이 불편합니다.

JSON 기반의 데이터베이스인데다가 NoSQL이기 때문에, 쿼리를 작성할 때 JSON으로 작성해야 합니다. 우선 간단하게 range 쿼리입니다.

아직까진 나쁘지 않죠? 여기에 and 조건을 좀 넣어볼까요?

이제 여기서 여러개의 조건들을 boolean operator로 묶으면…

슬슬 눈에 안들어오기 시작합니다. 하지만 진짜 최종보스는 SQL의 group by와 유사한 aggregation pipeline입니다.

daetul_mung

이런 쿼리를 쓰다가 괄호라도 하나 삐끗하면…

 

멘붕
대략 이런 기분을 느낄 수 있습니다.

그나마 예제라 조금 간단한데, 제대로 format 맞춰서 조금 복잡한 쿼리를 짜기 시작하면 열줄이 넘어가는 경우도 허다합니다. (실제로 Hey 통계 페이지에는 수십 줄짜리 쿼리가 있다는 소문이…) 개발자들은 배워서 쓰고 익숙해지다보면 어느 정도 해결 되는 문제이지만, 서비스 기획자들이 개발자를 통하지 않고 통계를 내보기는 쉽지 않습니다. 이런 쿼리를 실제로 쓸 일이 있냐구요? 네….  있더라구요.

물론 NoSQL이라 조인이 안되는건 보너스입니다. 모든 쿼리는 단일 콜렉션 내에서 해결하셔야 합니다. 

20111216171940

야! 신난다!

 

4. Subdocument가 계속해서 늘어날 땐 별도의 collection으로 만들고 인덱스를 거세요.

Join이 안되는 MongoDB에서 1:N 관계를 표현할 때에는 referencing과 embedding의 두 가지 방법이 있습니다.

예를 들어 여러 명의 여자친구가 있는 어떤 남자를 document로 표현한다고 생각해봅시다. (예제니까요) 그럼 아마 male 콜렉션에는 이런 데이터가 있을 것입니다. 

(1)번의 referencing으로 이 남자를 표현해 보겠습니다.

(2)번의 embedding은 관계가 있는 문서를 상위 document에 포함해버리는 방법입니다. 아래처럼요.

하지만 둘 다 계속해서 girlfriends의 숫자를 늘리는 데에 좋은 방법은 아닙니다. (말이 좀 이상하지만) 그 이유는 다음과 같습니다.

(1) MongoDB의 document는 최대 size가 제한되어 있습니다. Default는 16MB이고 조정이 가능하지만, 계속해서 늘릴 수는 없습니다.

(2) Document의 크기를 늘리는 operation은 새 document를 insert하는 operation에 비해 매우 느립니다. 사실 이는 MongoDB 내부에서 document growth를 다루는 방식의 문제이기 때문에 size가 늘어나는 모든 document에 해당되는 문제입니다.

따라서 1:N의 관계를 표현할땐 다음처럼 전통적인 RDBMS의 방법을 사용하는 것이 좋습니다.

물론 female 콜렉션의 male_id에 인덱스를 걸어두는 게 성능이 좋겠죠? 이렇게 하면 두 콜렉션을 동시에 가져올수는 없고, 두 콜렉션을 별도로 가져와서 어플리케이션 레벨에서 조인을 수행해야하는 슬픔이 있습니다… 역시 세상에 공짜같은건 없다는 진리를 다시 한번 느낄 수 있죠.

 

5. ActiveRecord와의 연동이 깔끔하지 않습니다.

Hey에서는 Ruby on Rails의 RDBMS 기반 ORM인 Activerecord와, MongoDB기반 ORM인 Mongoid를 동시에 사용합니다. 따라서, 당연하게도, ActiveRecord object에서는 mongoid와의 association을 선언할 수 없습니다. 따라서 다음과 같은 임시방편이 필요합니다.

이정도면 그나마 간단한 경우입니다. 만약 ActiveRecord object 하나와 연결된 여러 개의 Mongoid object를 하나의 transaction으로 묶어서 destroy해야 한다면 어떨까요?

그럴 듯 해보이나요? 하지만 위의 코드는 User를 지우다가 exception이 발생하면 Avatar만 지워지고 User만 남아있는 무시무시한 사태가 발생하게 됩니다. 이런 경우를 모두 생각해야 해서, 두 모델을 섞어서 쓰는 것은 그리 간단하지 않습니다. 쿼리도 마찬가지입니다. Avatar를 3개 이상 가진 User를 모두 반환하는 쿼리는 어떻게 작성하면 될까요? 이건 읽는 분들의 상상에 맡기도록 하겠습니다…

Tenacity같은 gem을 사용하면 조금 더 깔끔하게 되지 않을까 하는 생각도 들지만, 사용해보지 않아서 이렇다 할 말씀은 드리기가 힘드네요. 개인적인 판단으로는 redis와 같은 caching 용도를 제외하고 하나의 어플리케이션에서 두 가지 이상의 데이터베이스를 동시에 사용하는 것 자체가 여러 언어를 사용해서 하나의 어플리케이션을 만드는 것처럼 헬게이트를 여는 일인 것 같습니다. 잘 생각해보시고 정말 필요한 경우가 아니라면 다시 한번 고려해보시길 권고드립니다.

 

맺음말

쓰고 나니 MongoDB의 단점만 잔뜩 늘어놓은 것 같은 기분이 드는 건 기분 탓일까요? ^^; 아무래도 RDBMS에 익숙한 대부분의 개발자들에게 NoSQL은 너무 안되는 게 많은 스토리지처럼 보이는게 현실인 것 같습니다. 개인적인 경험에 비추어 보았을 때에는 MongoDB를, 더 나아가서 NoSQL을 도입할 때에는 스토리지 엔진의 특성을 잘 파악하고 꼭 필요한 경우에만 도입하는 것이 정답인 것 같습니다. 제가 생각하는 MongoDB가 적합한 케이스는 다음과 같습니다.

(1) Schema-free 한 데이터 구조가 꼭! 필요한 경우
Schema가 없다는 건 RDBMS에서는 얻기 정말 힘든 장점임에 틀림없습니다.

(2) Collection간의 관계가 그다지 긴밀하지 않을 경우
다른 collection과의 조인이 지옥이라는 건 위에서 충분히 보여드린 바 있죠….

(3) Insert 된 이후에는 document growth가 잘 일어나지 않는 경우 : document growth는 느립니다.

(4) Sharding이 꼭! 필요한 경우
개인적인 경험으로는 row 수가 억단위가 되지 않는 이상은 RDBMS의 인덱스 최적화와 테이블 파티셔닝으로 충분히 커버가 가능하다고 생각합니다. 사실 Hey에서 매칭 데이터를 MongoDB에 저장하기로 한 것도 매칭 수가 억단위를 넘어갈 경우를 생각해서였습니다만……

slamdunk

Hey에서 sharding이 필요할 정도로 row 수가 많아지는 일은 없었습니다…… (아직까지는요)

(5) 다른 데이터베이스와의 연결이 없이 MongoDB만으로 충분한 경우

 

그냥 하루빨리 오픈소스 진영에서 scale-out이 가능한 RDBMS를 내놓았으면 하는게 개인적인 바람입니다.

긴 글을 여기까지 읽어주신 분들께 감사드립니다. 끝으로 NoSQL에 대해 읽어볼만한 몇 가지 글들의 링크를 남기고, 저는 이만 물러가도록 하겠습니다.

 

NoSQL은 생각보다 쓸만하지 않다

Why does Quora use MySQL as the data store instead of NoSQLs such as Cassandra, MongoDB, or CouchDB?

Cassandra vs MongoDB vs CouchDB vs Redis

 

1425563_647984125251871_1119116285_n

Lethe

서버 사이드 기술과 데이터 핸들링 부분에 관심이 많은 초보 개발자입니다.
이것 저것 많이 공부해보고 있는데 공부할 건 많고 시간은 없고 몸뚱아리는 게을러서 슬프네요.

멘붕없이 RVM과 루비 설치하기

안녕하세요. 이음소시어스 개발팀에서 서버사이드 코딩을 하고 있는 김민규(이터)입니다. 저희 개발팀 블로그의 첫 글에 많은 관심을 가져주셨는데요, 두 번째 글은 딱히 특별한 기술은 아니지만 RVM과 루비 설치법에 대해 한글로 된 문서가 별로 없고, 설치하며 겪은 경험을 공유하면 좋을 것 같아 이 포스트를 작성하게 되었습니다.

RVM이란?

루비 좀 안다고 하는 사람들에게 루비를 어떻게 설치하냐고 물어보면 RVM으로 설치하라는 이야기를 많이 들을 수 있습니다. RVM은 Ruby Version Manager의 약자로, 여러 버전의 루비를 쉽게 관리할 수 있게 해주는 스크립트입니다. 

한 시스템에 여러 버전의 루비를 설치하기는 쉽지 않습니다. 일반적인 리눅스의 패키지 시스템으로 동시에 여러 버전의 루비를 깔면 환경변수 꼬이고 gem 꼬이고 난리도 아니죠. 그렇기 때문에 시스템에 이미 작업하고 있는 프로젝트가 있을 때 새로 릴리즈된 루비 버전을 사용해보기는 쉽지 않습니다. 이미 구버전 루비에서 작업한 기존 프로젝트가 어떻게 될지 모르기 때문이죠. 하지만 rvm을 사용하면 여러 버전의 루비, 심지어 gem도 따로 관리해주기 때문에 새 버전을 써보고 다시 전환하기가 쉽습니다. 여기에 더해 gemset 기능을 사용하면 프로젝트별로 별도의 루비 버전을 사용하는 일도 가능합니다.

rvm의 설치 자체는 홈페이지에 있는 스크립트를 그대로 실행하면 되는 아주 간단한 작업입니다. 다만 몇 가지 주의해야 할 사항들이 있는데 이런 것들을 모른 채 설치하면 메뉴얼대로 작동하지 않고, “분명 시키는 대로 했는데 왜 안돼!” 하고 rvm은 물론 루비는 매우 설정이 거지같은 언어라는 안타까운 편견에 빠지게 됩니다.저도 처음엔 고생 꽤나 했구요…

서버 세팅하다 꼬이거나 CSS를 짜다 보면 이런 기분이 듭니다.

이 포스트는 ubuntu 12.04 64bit 기준으로 작성되었습니다.

RVM 설치하기…전에 하면 안되는 일들!

인스톨 방법을 쓰기 전에, 먼저 잘못된 설치법부터 설명하겠습니다.

이 포스트에서 제일 중요한 내용입니다.
절대, 네버, 무슨 일이 있어도 이렇게 깔지 마세요!

apt-get으로 까는 사람들이 많을 수 밖에 없는 이유

우분투가 rvm이 없으면 이렇게 깔라고 친절하게 인도해줘서 속는 사람들이 너무 많습니다. ㅠㅠ

우분투의 apt는 대부분의 패키지들을 apt-get install 명령어 하나로 한방에 깔 수 있게 해주는 멋진 도구입니다. 그런데 이 rvm을 apt-get으로 깔면 무슨 일이 생기냐구요?

설치 테스트와 트롤링을 위해 준비된 VM들

숙련된 VM의 시범을 한번 보겠습니다.

이 포스트를 작성하는 현재 RVM의 최신 버전은 1.24.7입니다…

낡아도 이렇게 낡을 수가 없습니다…

‘구버전이라도 기능만 잘 돌아가면 되겠지?’ 하고 루비를 깔아보겠습니다.

원래 ruby-1.9.3-pxxx등으로 patch level이 쓰여져야 하는데 1.9.3-에서 끝나네요

잘못된 경로로 찾아가서 루비를 제대로 깔 수도 없습니다.
2.0같은건 아예 리스트에 존재하지도 않아요… ㅠㅠ

‘구버전이니까 업데이트를 하면 되겠지? 업데이트를 해보자!’

 

그냥 포기하고 멀쩡한 버전을 깔아봅니다.

클릭하시면 스택오버플로우 링크로 이동합니다

심지어 멀쩡한 버전을 깔려는데도 얘가 방해를 하고 있어서 치워줘야 합니다.
혹시 apt-get으로 rvm을 까셨다면 싹싹 지워줍니다.

1. 패키지를 apt-get –purge로 날려주고, /usr/share, /etc/ 에 있는 스크립트들을 지워줍니다.

싹싹 지워서, 시스템에 후환(?)이 없도록 합시다.

2. 삭제 후에는 shell을 재실행해 이미 로드된 환경변수들이 다시 사라지도록 해줍니다.

하루빨리 우분투에서 이 거지같은 패키지가 사라졌으면 하는 바람입니다.

RVM 진짜로 설치하기

RVM의 설치에는 두 가지가 있습니다. 잘 읽고 원하는 방법을 골라 설치하세요.

Single User Install

  • 설치한 유저에게만 RVM이 깔립니다.
  • 설치한 RVM은 당연히 ‘그 유저’만 쓸 수 있고, 다른 유저가 사용할 수 없습니다.
  • RVM은 설치한 유저의 홈 디렉토리의 .rvm폴더 ($HOME/.rvm)에 설치되고,
    RVM으로 설치하는 ruby와 모든 gem들도 $HOME/.rvm에 저장됩니다.
  • 여러 유저가 각각 Single User Install을 하면 모든 ruby와 gem들은 별도로 저장됩니다.
    시스템 공용으로 사용하는 것은 rvm install에서 루비를 설치하기 위해 설치한 라이브러리뿐입니다.
  • root계정으로 Single User Install을 할 수 없습니다.
    root permission을 가진 상태면 자동으로 multi user 인스톨을 하므로,
    root계정에만 rvm을 깔아서 쓰는 것은 불가능하진…않은데 이 포스트에서는 다루지 않습니다.
    root 계정에만 rvm Single user Install로 설치하기

Multi User Install

  • /usr/local/rvm/ 에 모든 rvm, ruby, gem이 설치됩니다.
  • 한 유저가 설치한 루비, 또는 gem은 시스템 전체에 공유됩니다.
  • 시스템의 모든 유저가 rvm, rvm으로 설치한 루비와 gem을 사용 가능합니다.
  • rvm그룹의 유저만 새 루비 또는 gem을 설치할 수 있습니다.

자 고르셨나요? 그럼 따라해 봅시다.

Single User Install 방법

1. root가 아닌 일반 유저로 아래 명령을 실행합니다. (\ 오타 아닙니다!!)

2. 쉘을 껐다 켜거나 rvm스크립트를 현재 쉘에 로드해줍니다.

끝! 쉽죠?

Multi User Install 방법

1. root가 아닌 일반 유저로 아래 명령을 실행합니다. (\ 오타 아닙니다!!)

bash 앞에 sudo만 붙여주면 됩니다. 와!

실제로 root로 설치해 봤을때 특별히 이상한 점은 찾지 못했는데,
rvm 문서를 보면 root와 일반 유저의 환경변수 차이 때문에
root로 직접 깔지 말고 일반 유저에서 sudo를 써서 사용하라고 합니다.
시키는 대로 하는게 편하겠죠?

2. 그 다음 RVM을 사용할 유저들을 rvm 그룹에 추가합니다.

그룹에 추가를 안해주면 /usr/local/rvm에 write권한이 없어서 루비 설치가 불가능합니다

일반 유저는 /usr/local/rvm에 write권한이 없기 때문에 rvm 그룹에 추가해서 권한을 부여해야 합니다.
이게 없으면 rvm install등을 할 때 권한이 없다고 죽어버려요.

3. 쉘을 껐다 켜거나
(쉘을 껐다 켤 수 없는 상황이면) 그룹 권한을 재로드하고, rvm스크립트를 현재 쉘에 로드해줍니다.

이번에는 그룹 권한도 다시 로드 해야해서 좀 귀찮습니다.
그래서 쉘을 그냥 껐다 켜는게 편해요.

축하합니다! RVM 설치가 끝났습니다!

RVM으로 Ruby 설치하기

sudo같은거 붙일 필요도 없고 붙여서도 안됩니다.
이 과정에서 permission denied가 뜨면 대부분 그룹 문제이기 때문에
id를 쳐서 유저가 rvm 그룹인지 확인해보세요.

이미지가 너무 길어서 잘 안보이는데 끝에 보면 1001(rvm) 이렇게 써있습니다 ㅠㅠ
그룹에 추가했는데도 없으면 쉘을 재실행하거나 위에서 알려준 커맨드로 그룹 권한을 재로드해보세요.

설치 도중 sudo권한이 필요하면 알아서 물어봅니다.

설치 중 시스템 업데이트가 필요해 sudo권한이 필요한 경우가 있는데, 이 경우엔 알아서 물어봅니다.
rvm커맨드에 따로 먼저 sudo를 붙여야 하는 경우는 전혀 없으니,
혹시 permission denied가 뜨면 그룹을 먼저 확인해주세요.

네, 이제 루비 코딩을 시작하시면 됩니다!

TroubleShooting

Q: 루비 스크립트에서 sudo권한이 필요한데, sudo를 쓰면 rvm을 찾을 수 없다고 나오네요. 어쩌죠?

Single User Install에서 볼 수 있는 현상인데, rvm은 우리의 로그인 쉘에 로드되는 스크립트이기 때문에 root에는 깔려있지 않고 깔려 있다 하더라도 그 스크립트를 로드하지 않습니다 ㅠㅠ

sudo 대신 rvmsudo를 사용하면 현재 쉘의 루비를 가지고 sudo권한으로 실행할 수 있습니다.

Q: sudo -s,  su [username]등으로 유저를 바꾸고 그 유저의 rvm을 쓸 수 없나요?

sudo -s, su [username]은 로그인 쉘이 아니라 스크립트를 로드하지 않습니다.
su – [username] 으로 하면 그 유저의 로그인 쉘을 띄우기 때문에 그 유저의 rvm을 쓸 수 있습니다.

Q: 이미 시스템 루비가 있는데 지워야 하나요?

rvm 경로가 $PATH의 가장 앞에 붙기 때문에 시스템 루비는 남아있어도 상관없습니다.

 

이상, 한국에 루비 사용자가 한 명이라도 늘기를 바라는 이터였습니다.