본문 바로가기
DB,SQL

ZooKeeper를 활용한 Redis Cluster 관리

by violetoz 2014. 3. 21.

NHN Business Platform 클라우드플랫폼개발랩 임영완, 배상용

지속적으로 늘어나는 푸시 사용자를 MySQL 샤딩으로만 감당하기에는 버거웠습니다. 그래서 다양하게 검토한 끝에 MySQL을 대체할 데이타베이스로 Redis를 선택하게 되었고, 클러스터를 구성하기 위해서 ZooKeeper라는 도구를 사용했습니다. 이 글에서는 Redis와 ZooKeeper의 조합으로 Redis Cluster를 구성하는 방법을 알아보겠습니다.

Redis를 메시지 데이터베이스로 사용하게 된 배경

NNI(NHN Notication Infrastructure)는 Android 운영체제 기반의 스마트폰 애플리케이션에 푸시 알림을 제공하는 NHN의 플랫폼이다. NNI는 푸시 알림을 저장하는 저장소로 MySQL을 사용하고 있었다. NNI 시스템 구조상 서비스의 사용량이 많아 데이터베이스를 증설할 경우, 다수의 MySQL 연관 장비를 투입해야 함과 동시에 데이터베이스 세트가 추가될 때마다 애플리케이션 서버의 샤딩(sharding) 로직을 수정해 서버를 다시 배포해야 하는 문제가 있었다.

이러한 문제점을 개선하기 위해 푸시 알림 메시지 형태의 자료를 저장하기에 적합한 구조를 지원하고, 횡적 확장이 용이하며, 알림 메시지의 저장 및 검색 성능과 관련한 기능에서는 MySQL보다 뛰어난 솔루션에 대해 검토를 진행하게 되었다.

여러 가지 솔루션을 검토한 후 NHN의 서비스에서는 대용량의 데이터를 저장하기보다 높은 트래픽을 처리하는 것이 가장 중요한 포인트라고 생각했고, RDBMS보다 MemoryDB를 선택하게 되었다. 그리고 1년이 넘는 시간 동안 운영하면서 단 한 차례도 장애를 일으키지 않은 Redis를 신뢰해 MySQL 대용으로 사용하기로 했다.

그러나 Redis 선택 시 몇 가지 생각해야 될 문제가 있다. 첫 번째로 가장 불편한 사항은 샤딩을 지원하지 않는다는 점이다. 확장성(scalability)을 확보하려면 수동으로 샤드(shard)를 구성한 후 애플리케이션 서버에서 해싱(hashing) 로직에 의해 샤딩을 지원해야 하고 클러스터 관리 도구도 직접 만들어야 한다.

두 번째로 클러스터 구성에서 중요한 Failover 기능이 없다는 점이다. Master-Slave Replication 구성 시 Master가 장애가 날 때 자동으로 처리하지 않아 일일이 수동으로 복구해야 한다.

Redis Cluster 구성을 위한 ZooKeeper 도입 배경

Redis로 클러스터를 구성해서 사용하려면 앞에서 언급한 문제점을 해결해야 한다. 분산 서버 환경에서 ZooKeeper가 필수라고 많이 이야기하고, 사내에서 Line과 Arcus 등의 서비스에서 이미 안정성이 검증됐기 때문에 크게 의심하지 않고 ZooKeeper를 선택했다.

분산 처리 환경에서 필수로 언급되는 ZooKeeper란 무엇일까? 한 마디로 정의하면 "분산 처리 환경에서 사용 가능한 데이터 저장소"라고 말할 수 있겠다. 기능은 매우 단순하지만 분산 서버 환경에서는 활용 분야가 넓다. 예를 들어 분산 서버 간의 정보 공유, 서버 투입/제거 시 이벤트 처리, 서버 모니터링, 시스템 관리, 분산 락 처리, 장애 상황 판단 등 다양한 분야에서 활용할 수 있다.

'어떻게 다양한 활용이 가능할까?'라는 의문을 해결하기 위해서 기능에 대해서 한 마디로 설명하면 다음과 같이 설명할 수 있다.

ZooKeeper는 데이터를 디렉터리 구조로 저장하고, 데이터가 변경되면 클라이언트에게 어떤 노드가 변경됐는지 콜백을 통해서 알려준다. 데이터를 저장할 때 해당 세션이 유효한 동안 데이터가 저장되는 Ephemeral Node라는 것이 존재하고, 데이터를 저장하는 순서에 따라 자동으로 일련번호(sequence number)가 붙는 Sequence Node라는 것도 존재한다. 조금 과장하면 이러한 기능이 ZooKeeper 기능의 전부다. 이런 심플한 기능을 가지고 자신의 입맛에 맞게 확장해서 사용하면 된다.

기능을 하나씩 자세히 살펴보자. 우선 ZooKeeper가 데이터를 저장하는 방식인 데이타 모델에 대해서 살펴보자.

ZooKeeper는 <그림 1>과 같은 디렉터리 구조로 데이터를 저장한다. 특징을 살펴보면 Persistent를 유지하기 위해서 트랜잭션 로그와 스냅샷 파일이 디스크에 저장되어 시스템을 재시작해도 데이터가 유지된다. 각각의 디렉터리 노드를 znode라고 명명하며, 한 번 설정한 이름은 변경할 수 없고 스페이스를 포함할 수도 없다.

cb5d7850b5b97cda5707521ada7cbf71.png

그림 1 ZooKeeper의 데이터 모델(이미지 출처:http://zookeeper.apache.org/doc/trunk/zookeeperOver.html)

데이터를 저장하는 기본 단위인 노드에는 Persistent Node와 Ephemeral Node, Sequence Node, 3가지가 있다.

첫 번째로 Persistent Node는 한 번 저장되고 나면 세션이 종료되어도 삭제되지 않고 유지되는 노드다. 즉, 명시적으로 삭제하지 않는 한 해당 데이터는 삭제 및 변경되지 않는다.

두 번째로 Ephemeral Node는 특정 노드를 생성한 세션이 유효한 동안 그 노드의 데이터가 유효한 노드다. 좀 더 자세히 설명하면 ZooKeeper Server에 접속한 클라이언트가 특정 노드를 Ephermeral Node로 생성했다면 그 클라이언트와 서버 간의 세션이 끊어지면, 즉 클라이언트와 서버 간의 Ping을 제대로 처리하지 못한다면 해당 노드는 자동으로 삭제된다. 이 기능을 통해 클라이언트가 동작하는지 여부를 쉽게 판단할 수 있다.

세 번째로 Sequence Node는 노드 생성 시 sequence number가 자동으로 붙는 노드다. 이 기능을 활용해 분산 락 등을 구현할 수 있다.

다음으로 ZooKeeper 서버 구성도를 살펴보자.

8bfee7227f1fd09e4ec7439249870b32.png

그림 2 ZooKeeper의 서버 구성도(이미지 출처:http://zookeeper.apache.org/doc/trunk/zookeeperOver.html)

ZooKeeper 서버는 일반적으로 3대 이상을 사용하며 서버 수는 주로 홀수로 구성한다. 서버 간의 데이터 불일치가 발생하면 데이터 보정이 필요한데 이때 과반수의 룰을 적용하기 때문에 서버를 홀수로 구성하는 것이 데이터 정합성 측면에서 유리하다. 그리고 ZooKeeper 서버는 Leader와 Follower로 구성되어 있다. 서버들끼리 자동으로 Leader를 선정하며 모든 데이터 저장을 주도한다.

클라이언트에서 Server(Follower)로 데이터 저장을 시도할 때 Server(Follower) -> Server(Leader) -> 나머지 Server(Follower)로 데이터를 전달하는 구조이고, 모든 서버에 동일한 데이터가 저장된 후 클라이언트에게 성공/실패 여부를 알려주는 동기 방식으로 작동한다. 즉, 모든 서버는 동일한 데이터를 각각 가지고 있고, 클라이언트가 어떤 서버에 연결되어 데이터를 가져가더라도 동일한 데이터를 가져가게 된다.

사용/운영 시 몇 가지 주의 사항이 있다. 개발에 급급한 나머지 ZooKeeper를 캐시 용도로 사용해 서비스를 오픈했다가 장애가 발생한 사례도 종종 있다고 한다. 데이터의 변경이 자주 발생하는 서비스에서 ZooKeeper를 데이터 저장소로 사용하는 것은 추천하지 않는다. ZooKeeper에서 추천하는 Read : Write 비율은 10 : 1 이상이다.

그리고 ZooKeeper 서버가 제대로 실행되지 않을 때가 있는데, 대부분 서버간의 데이터 불일치로 인한 데이터 동기화 실패가 그 원인이다. 주로 베타 테스트 후 운영 직전에 ZooKeeper 서버를 증설해서 사용하는데, 이럴 때 기존에 테스트했던 서버와 신규로 투입한 서버의 데이터 차이로 인해 이런 현상이 종종 발생한다. 이때는 데이터를 초기화한 후 서버를 실행하면 된다.

그리고 마지막으로, zoo.cfg라는 설정(configuration) 파일의 ZooKeeper 서버 목록을 꼼꼼히 확인해야 한다. 서버 목록이 정확히 맞지 않아도 서버가 실행되긴 하지만, 로그 파일을 확인하면 ZooKeeper 서버가 지속적으로 재시작할 때도 있고 데이터를 엉뚱한 곳에 저장하기도 한다.

아키텍처의 변화

ZooKeeper와 Redis를 이용해 Redis Cluster를 구성하기로 결정했고 아키텍처(architecture)에 대해서 수많은 아이디어를 검토했다. 크게 2가지 구성도를 검토했다.

<그림 3>은 1차 아키텍처 구성도다.

9ee69c124468e74377142402f61ffd4d.png

그림 3 1차 아키텍처 구성도

ZooKeeper에 저장되어 있는 Redis 서버 정보가 변경되면 애플리케이션 서버에 콜백으로 알려주고, 애플리케이션 서버는 해당 정보를 통해 Redis에 접속하는 구조다. Redis 서버에 문제가 발생하면 ZooKeeper가 감지하고(Ephemeral Node의 기능을 활용해 세션이 종료되면 ZooKeeper의 데이터가 자동으로 삭제되어서 감지함) 애플리케이션 서버에 콜백으로 알려준다. 이 구조의 장점은 ZooKeeper의 많은 기능을 활용할 수 있다는 것이다. 단점은 ZooKeeper가 단순히 Redis 서버의 네트워크 단절만을 확인한다는 것과 Redis의 Master/Slave를 관리하기 위해서는 애플리케이션 서버에서 직접 처리해야 한다는 것이다.

이러한 단점을 보안해 2차 아키텍처를 구성했다. 2차 아키텍처 구성은 <그림 4>와 같다.

da4b2fab7b0a411dd3310674fb54a396.png

그림 4 2차 아키텍처 구성도

2차 아키텍처에서는 ZooKeeper의 노드 변경 여부를 클라이언트에게 알려주는 Watcher 기능만을 사용했다. 1차 아키텍처 구성도에서 사용했던 Ephemeral Node는 사용하지 않고 Redis Cluster Manager에서 ZooKeeper의 Ephemeral Node 기능까지 직접 구현해 사용했다. 1차 아키텍처 구성도보다 애플리케이션 서버의 로직을 간소화할 수 있고, Redis Cluster Manager만 제대로 구현한다면 애플리케이션 서버와 Redis 서버 데몬 등에 새로운 로직을 구현할 필요가 없다.

Redis Cluster Manager의 요구 사항

Redis Cluster Manager를 개발하기 위한 요구 사항은 다음과 같다.

  • NPUSH-NNI의 메시지 데이터베이스 외에 여러 서비스에서 사용하는 Redis Cluster를 관리할 수 있어야 한다(예: NNI 단말 토큰용 Redis Cluster, NPUSH-GW의 Registration DB Cluster 등).
  • 한 클러스터는 다수의 샤드로 구성될 수 있다.
  • 한 개의 샤드는 다수의 Redis 서버로 구성될 수 있고 1개의 Master와 다수의 Slave로 구성된다.
  • 위의 기능을 지원하기 위해 ZooKeeper 디렉터리를 제어할 수 있는 RPC API를 제공한다.
  • 등록된 서버의 Health check를 주기적으로 수행하고 각 샤드의 Master 서버가 동작하지 않으면 Slave 서버를 자동으로 Master로 promotion하는 기능을 구현한다.
  • NCS 서버에게는 Redis의 상태 및 샤딩 정보를 알 필요 없는 캡슐화된 ShardedRedisClient 라이브러리를 제공한다.
    • Pushevent 메서드와 PusheventRes 메서드, Subscribe 메서드만 호출한다.
    • 샤딩 규칙(sharding rule) 변경에 따른 Old Hashed Data 및 New Hashed Data 모두 장애 없이 처리돼야 한다. 캐시 용도가 아니기 때문에 Consistent Hashing 로직 적용은 고려하지 않는다.

ZooKeeper 디렉터리 구조

위의 요구 사항을 만족시키기 위해 ZooKeeper의 분산 저장소 기능을 사용해 디렉터리 구조를 설계했다.

예제 1 ZooKeeper 디렉터리 구조

/status
- NHN-DB :
- shard-1 :
- "10.102.29.187:6380" : "normal"
- "10.102.30.220:6380" : "normal"
- "10.102.40.47:6380" : "normal"
- shard-2 :
- "10.102.43.60:6380" : "normal"
- "10.102.41.46:6380" : "normal"
- "10.102.43.199:6380" : "normal"
/clusters
- NHN-DB : NNI Message DB
- shard-1 : auto_recovery
- "10.102.29.187:6380" : "master"
- "10.102.30.220:6380" : "slave"
- "10.102.40.47:6380" : "standby"
- shard-2 : auto_recovery
- "10.102.43.60:6380" : "master"
- "10.102.41.46:6380" : "slave"
- "10.102.43.199:6380" : "backup"
/shard-rules
- NHN-DB : NNI Message DB
- status : plan-1
- plan-1 : "10.102.29.187:6380"
- plan-2 : "10.102.29.187:6380", "10.102.43.60:6380"

디렉터리 구조를 간단히 설명하면 /status 디렉터리는 Redis 서버의 상태를 기록하는 용도로 사용하고 /clusters 디렉터리는 해당 Redis 서버의 role을 기록하는 용도로 사용한다.

/shard-rules 디렉터리는 클러스터에 샤드가 추가될 때 기존 샤드 정보 및 신규 샤드 정보를 기록해 애플리케이션 서버에 제공되는 ShardedRedisClient에서 활용할 수 있도록 샤드 룰(shard rule) 정보를 제공한다.

이제 Redis Cluster Manager의 주요 컴포넌트에서 활용하는 ZooKeeper의 Watch 기능을 이용해 분산 디렉터리 정보를 어떻게 관리하고 애플리케이션 서버에서 어떻게 해당 디렉터리 정보를 이용할 수 있는지 알아보겠다.

Redis Cluster Manager의 시스템 구성

Redis Cluster Manager는 BLOC Container에서 수행되는 BLOC 모듈로 구현돼 있다. ZooKeeper 및 Redis 서버를 제어하기 위해 전용 클라이언트를 사용하는 BO(Business Object)/DAO(Data Access Object)를 구현했고, 구현한 BO 중 외부 API 형태로 노출해야 할 BO 메서드는 BLOC Resouce 형태로 작성해 노출시켜 주었다.

참고
BLOC(Business Logic Container)은 BO를 제공하는 NHN의 서비스 컨테이너다.

또한 Redis 서버의 Health check, Server failover, Sharding rule 관리를 담당하는 별도의 컴포넌트로 구성되어 있다.

86a50fa1ca4cc203b44094f884db256e.png

그림 5 Redis Cluster Manager의 주요 컴포넌트

Redis Cluster Manager의 주요 컴포넌트

BLOC BO API

Redis Cluster Manager 시스템 구성 섹션에서 ZooKeeper 디렉터리 및 Redis 서버를 관리하기 위해 BLOC Resource를 이용한다고 했다. 노출된 BLOC API 호출을 통해 Cluster, Shard, Node, Shardrule 정보를 업데이트할 수 있고 이를 통해 ZooKeeper의 분산 디렉터리 설정 및 Redis 서버의 role을 설정할 수 있는 기능을 수행한다.

Healthcheck Manager

Healthcheck Manager는 ZooKeeper 서버에 생성된 /status 디렉터리의 하위 디렉터리에 기록된 Redis 서버의 리스트를 읽어 주기적으로 해당 서버들로 redis ping command를 호출한다. Ping에 대한 응답 유/무에 따라 해당 서버의 정보가 기록된 노드의 데이터를 변경한다(normal <-> abnormal).

또한 /status 디렉터리의 하위 디렉터리에 대해 Watch 설정을 적용해서 Redis 서버가 추가/삭제 또는 상태가 변경되었을 경우 Ping Check를 위한 Healthcheck Thread를 초기화하는 기능을 수행한다.

다음은 Healthcheck Manager의 주요 기능이다.

Initialize

  1. /status 하위 디렉터리에 모두 Watch 설정을 적용해서 노드 관련 이벤트가 발생하는지 감시한다(노드의 추가, 수정, 삭제가 발생하는지 감시).
  2. 현재의 /status 디렉터리 아래의 노드 리스트(Redis 서버)를 읽어 주기적(2초)으로 Redis Java Client인 Jedis를 이용해 redis ping command를 실행한다.

ba8ad90b4b7300410aeb825302ca6bad.png

그림 6 Healthcheck Manager의 초기화 시나리오

Monitoring

  1. ping response time이 10초가 넘을 경우 바로 서버의 상태를 abnormal로 변경한다.
  2. connection 오류가 발생할 경우 3번 retry를 시도한다. retry 간격은 지수로 증가한다.
  3. retry를 시도해도 오류가 발생할 경우 서버 상태를 abnormal로 변경한다.
  4. 최대 Ping 주기 + retry 시도 시간 안에 서버 상태 변경이 발생한다(약 9초).

e5de3ce01d1759c65f4412decd561cf7.png

그림 7 Healthcheck Manager의 모니터링 시나리오

Watching

  1. /status의 하위 디렉터리가 변경되는지 항상 확인한다(노드 추가, 노드 삭제, 데이터 변경).
  2. 노드가 추가되거나 데이터가 변경된다면 ZooKeeper 서버로부터 Watch Event를 받는다.
  3. Watch Event를 받으면 현재 ScheduledThreadPoolExecutor에 의해 수행되고 있는 Health Check Thread를 종료함과 동시에 새로 Health Check Thread를 생성한다.
  4. Watch Event는 한번 발생하면 다시 Watch 설정을 할 때까지 발생하지 않으므로 다시 /status 하위 디렉터리에 대한 Watch 설정을 한다.

76465e185a8161bf8e05359ee7b9162a.png

그림 8 Healthcheck Manager의 Watching 시나리오

Cluster Manager

Cluster Manager 또한 ZooKeeper 서버에 생성된 /status 디렉터리의 하위 디렉터리에 Watch 설정을 적용하고 Redis 서버들의 상태 변화가 있을 경우 전달된 Watch Event를 받아 Redis 서버의 Failover, Standby 서버 전환 등을 통해 Redis 서버 클러스터를 관리하는 기능을 수행한다.

다음은 Cluster Manager의 주요 기능이다.

Initialize

  • /status 디렉터리의 하위 디렉터리에 모두 Watch 설정을 적용해서 노드 관련 이벤트가 발생하는지 감시한다.

3ed53c4d8b2d81a1ac1b2ce4102ec6c4.png

그림 9 Cluster Manager의 초기화 시나리오

Watching

  1. 노드가 추가되거나 데이터가 변경된다면 ZooKeeper 서버로부터 Watch Event를 받는다.
  2. Watch Event를 받으면 상태가 변경된 서버 중에 /clusters 디렉터리 아래 동일 서버를 확인하고 Master 서버의 상태가 변경되었는지 확인한다.

0d7df4000a7782c8d35e9334ee06981e.png

그림 10 Cluster Manager의 Watching 시나리오

Failover

  1. Master 서버가 abnormal 상태로 변경되면 다음과 같은 작업을 수행한다.
    1. 이전 Master 서버는 standby 상태로 변경한다.
    2. Slave 서버 중에 1개를 Master 상태로 변경한다.
    3. 변경할 Slave 서버가 없다면 백업 서버를 Slave 서버와 같은 절차로 Master로 변경한다.

    참고
    Master와 Slave 서버는 같은 IDC 내 다른 access switch에 구성했고, 백업 서버는 다른 IDC에 구성했다.

  2. ZooKeeper 디렉터리 정보를 모두 업데이트했으면, Redis Java Client인 Jedis로 slaveofNoOne 명령을 실행시켜 신규 Master가 될 Redis 서버를 Master role로 변경한다.

9af3ef1e8ffcb8ca5a2e12c722ad2e6a.png

그림 11 Cluster Manager의 Failover 시나리오

설정 팁

참고로 위와 같이 Cluster Manager의 Failover 기능을 정상적으로 이용하기 위해 NNI에서 사용하는 Redis 메시지 데이터베이스 서버는 다음과 같은 설정을 이용해 실행된다.

  • Master, Slave는 dump file off, Backup 서버는 dump on 설정을 한다. 아래 내용이 redis.conf 파일에 포함되어 있다면 dump on 설정이다. 아래 내용에 대해서 좀 더 자세히 살펴보면 900초 동안 1건의 데이터 변경이 발생하거나 300초 동안 10건의 데이터가 변경되거나 60초 동안 10000건의 데이터가 변경될 경우 디스크 저장을 시도하라는 옵션이다.

Save 900 1
Save 300 10
Save 60 10000

  • 장비 성능이 좋지 않은 경우 Loglevel을 verbose에서 notice로 변경할 필요가 있다. Verbose로 로그 레벨을 설정하면, 로그를 남기다가 Redis operation 성능 저하로 1~2초 정도 멈추는 현상이 발생할 수 있다.

Loglevel notice

Shardrule Manager

Shardrule Manager는 ZooKeeper 서버에 생성된 /clusters 하위 디렉터리를 Watch 설정을 통해 감시하고 있다가 클러스터 내 Master Redis 서버 정보가 변경되었을 경우 해당 Event를 받아 /shard-rules의 plan-1, plan-2 노드를 업데이트하는 기능을 수행한다.

plan-1 및 plan-2 노드의 데이터는 Shard별 Master IP를 콤마 구분자로 구분하는 데이터이며 애플리케이션 서버에 제공된 ShardedRedisClient에서 해당 정보를 이용해 서버 Hashing을 하는 정보로 활용되고 있다.

애플리케이션 서버(NPUSH-NNI의 NCS 서버)는 /shard-rules 하위 디렉터리를 Watching하고 있다가 Master 서버의 IP가 변경될 경우 신규 Master 서버로 접속하게 된다.

다음은 Shardrule Manager의 주요 기능이다.

Initialize

  • /clusters 디렉터리의 하위 디렉터리에 모두 Watch 설정을 적용해 노드 관련 이벤트가 발생하는지 감시한다.

5dcc4f234840de63adae2d4935e4c55d.png

그림 12 Shardrule Manager의 초기화 시나리오

Watching

  1. /clusters 디렉터리 아래 노드가 추가되거나 데이터가 변경된다면 ZooKeeper 서버로부터 Watch Event를 받는다.
  2. /clusters 디렉터리 아래의 Master 노드 정보와 shard-rules 노드 아래 plan-1, plan-2 노드의 데이터로 있는 Master IP 정보를 비교해 plan-1, plan-2의 데이터가 Master IP와 일치하는지 확인한다.
  3. 만일 일치하지 않는다면 shard-rules 노드 아래 plan-1, plan-2 노드의 데이터를 신규 Master IP 정보로 업데이트한다.

f04c5397606ff9a30ed9fb5789bd929b.png

그림 13 Shardrule Manager의 Watching 시나리오

마치며

Redis 데이터베이스를 클러스터 형태로 관리하는 것은 위에서 설명한 Redis Cluster Manager를 통한 방법 외에도 다양한 방법으로 구현할 수 있다.

하지만 현재 시점에서 Redis 데이터베이스를 사용하거나 운영하는 사례가 많아진다면 우리와 같은 이슈에 대해서 고민하는 사람들이 많아질 것으로 예상한다. 따라서 우리가 개발한 사례를 좀 더 자세히 공유해 Redis 데이터베이스를 운영할 때 참조할 수 있는 자료로 활용할 수 있을 것이다.

Redis 데이터베이스는 RDBMS와는 달리 팀에서 자체적으로 필요에 따라 적용해 설치, 서비스 반영, 운영 등을 시작하고 있는 단계이다. 이러한 상황에서 더 전문적이고 체계적인 운영 기술 및 노하우가 취합되고 공유되었으면 하는 바람이다.

참고 자료


출처 : http://helloworld.naver.com/helloworld/294797


'DB,SQL' 카테고리의 다른 글

Mongodb sharding  (0) 2014.06.16
Redis 기초 명령어  (0) 2014.03.25
php 에서 mongoDB 사용하기  (0) 2014.03.07
php와 mongodb 연동  (0) 2014.03.05
mongoDB php연동  (0) 2014.03.05