블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

Couchbase Server

#1 소개 및 설치

조대협 http://bcho.tistory.com


근래에 여러 NoSQL이 소개되었지만 그중에서 좋은 솔루션인데도 불구하고 그다지 국내에서는 널리 알려지지 않은 카우치베이스에 대해서 소개하고자한다. 모바일 게임중에 유명한 쿠키런의 경우 카우치베이스를 백엔드로 사용하고 있는데, 안정성이나 성능등이 매우 뛰어나고, 사용하기 또한 매우 쉽다. 오늘은 고성능 NoSQL 서버인 카우치베이스(CouchBase) 에 대해서 소개하고자 한다.


소개


예전에 메모리 캐쉬 솔루션인 memcached에 디스크 persistence 기능을 추가하여 membase라는 솔루션이 있었는데, 이 제품에 Apache의 카우치디비(CouchDB)를 기반으로 새롭게 만든 솔루션이 카우치베이스 Server 라는 NoSQL 솔루션이다.

카우치베이스는 mongoDB나, Riak과 같이 JSON document를 직접 저장할 수 있는 Document DB 형태를 가지며, NoSQL의 분산 이론인 CAP theorem에서 CP (Consistency & Partition tolerance) 의 부분에 해당하여 데이타에 대한 일관성과, 노드간의 네트워크 장애시에도 서비스를 제공할 수 있다. 근래에 들어서 600억원의 투자를 유치하는 등 가치를 인정 받고 있는데, mongoDB나 Cassandra에 가려서 그다지 주목을 받지 못하는 것 같아서, 이번 글을 통해서 소개하고자한다.


특장점


카우치 베이스는 다른 NoSQL에 비해서 다음과 같은 추가적인 특징을 더 가지고 있다.


Memcached 기반의 Level 2 캐쉬를 내장하여 빠름

카우치베이스는 앞에서도 설명하였듯이 membase를 기반으로 하였기 때문에, memcached를 자체적으로 Level 2 캐쉬로 사용하고 있다. 즉 자체적으로 메모리 캐쉬 기능을 가지고 있기 때문에 성능이 대단히 빠르다. 이번에 카우치베이스 社에서 발표한 자료에 따르면 mongoDB대비 약 6배 이상의 성능을 낸다고 한다. 


(http://info.couchbase.com/2014-Benchmark-Showdown-Results-LP.html)


모바일 디바이스와 Sync

카우치베이스 뿐만 아니라, 카우치디비 계열 DB들은 iPhone이나 Android와 같은 모바일 디바이스에 탑재 할 수 있고, 서버에 설치되 카우치디비 계열들과 Sync가 가능하다. 카우치베이스도 역시, 카우치디비 계열이기 때문에, 모바일 디바이스에 탑재할 수 있고, 서버와 Sync할 수 가 있다.


데이타 센터간 복제 가능

카우치베이스는 XACR(Cross Data Center Replication)이라는 기능을 이용하여, 물리적으로 떨어진 데이타 센터간에 데이타 복제가 가능하다.


Indexing, Grouping ,Ordering,Join 가능

아주 중요한 특징중의 하나인데, 대부분의 NoSQL은 Key/Value Store 형식으로, 개별 필드에 대한 Indexing이나, 필드별로 group by 를 해서 sum,count등을 하는 기능이나, 특정 필드별로 Sorting이 불가능하다. Indexing을 지원하는 경우도 있기는 하지만, 내부적으로 성능상 문제가 있는 경우가 많은데, 카우치베이스의 경우 이러한 성능상 문제를 해결 하면서도 RDBMS들이 지원하는 index, grouping, ordering 기능을 지원할 수 있다. 


확장이 쉬움

보통 분산 구조의 NoSQL의 경우, 노드를 확장하거나 특정 노드가 장애가 났을때의 처리가 어려운데, 카우치베이스는 장애가 손쉽게 장애 처리를 하고,새로운 노드를 추가할때 도 매우 쉽게 노드 추가가 가능하다. 이러한 장점은 운영 관점에서 큰 이점이 된다.


Built in 관리 도구 제공

마지막으로 카우치베이스는 웹 기반의 GUI 관리 도구를 기본으로 제공한다. 많은 NoSQL들이 별도의 관리, 모니터링 도구를 지원하지 않는데 반하여, 기본적으로 강력한 관리 도구를 제공하는 것은 큰 장점이 될 수 있다.


Memcached 프로토콜 지원

캐쉬 솔루션으로 유명한 Memcached 프로토콜을 그대로 지원하기 때문에, 기존의 Memcached 클라이언트를 그대로 사용할 수 있고, 기존에 사용하던 Memcached 인프라를 그대로 대체 할 수 있다.


스키마가 없는 유연한 저장 구조 (Scheme-less)

 스키마가 없는 구조는 카우치베이스뿐만 아니라 대부분의 NoSQL이 갖는 공통적인 특성이다. 스키마가 없기 때문에 하나의 테이블에 컬럼 형식이 다른 데이타를 넣을 수 있다. 즉 하나의 데이타 버켓에 데이타 구조가 다른 JSON 문서들을 넣을 수 있다는 이야기이다.

데이타 타입이 다름에도 불구하고, 공통되는 필드에 대해서, Indexing, grouping 등을 제공할 수 있다. JSON 도큐먼트에, county 라는 앨리먼트가 있는 도큐먼트들을 대상으로 grouping등을 할 수 있다는 이야기이다.

다양한 클라이언트 플랫폼 지원

자바,닷넷,PHP,루비,C,파이썬,node.js 등 다양한 클라이언트 라이브러리를 제공한다. 클라이언트 SDK는 http://www.couchbase.com/communities/all-client-libraries 에서 다운로드 받을 수 있다.


설치하기


카우치베이스를 설치하기 위해서는 www.couchbase.com에서 카우치베이스를 맞는 OS 플랫폼에 따라서 다운로드 받으면 된다. Enterprise Edition과 Community Edition이 있는데, Enterprise Edition은 별도의 상용 라이센스를 구입해야 하며 기술 지원등을 받을 수 있다. Community Edition은 무료로 개발이나 운영 환경에 사용할 수 있으나, 기술 지원등을 받을 수 없고,  Enterprise Edition에 비해서 버전이 낮다.

※ 참고로, 상용과 오픈소스 라이센스 정책을 함께 지원하는 솔루션의 경우에는 버전업이 되면서 라이센스 정책이 갑자기 바뀌는 경우가 많으니, 오픈소스의 경우 다운로드 전에 반드시 라이센스 정책을 확인하기를 바란다. 이 글을 쓰는 현재 Community Edition의 라이센스 정책 기준은 2.2.0을 기준으로 한다. 

여기서는 윈도우즈 환경을 기준으로 설명을 한다. 사이트에서 카우치베이스 server 를 다운로드 받고 실행을 하면 자동으로 설치 위자드가 실행되고, 설치가 진행된다.

 


설치가 다 끝나면, 자동으로 웹페이지가 열리면서, 카우치베이스에 대한 셋업이 시작된다.

 


설정 셋업을 시작하면 기본적인 서버 설정에 대해서 물어보는데

 


데이타 파일을 저장하는 경로와, 호스트명등을 물어본다.그리고 새로운 클러스터를 시작할지, 아니면 기존의 클러스터에 조인할지를 물어보는데, 여기서는 개인 개발 환경을 설정하는 것이기 때문에, “Start a new cluster”로 설정한다. 이때, 카우치베이스가 사용할 메모리 용량을 지정해야 한다. 개인 개발환경이기 때문에, 1G정도로 설정하자. 실제 운영환경에서는 최대한 크게 잡아줘야 한다. 카우치베이스에 저장되는 키와 데이타에 대한 메타 데이타는 모두 메모리로 로딩되기 때문에, 메모리 용량이 충분하지 않으면 제대로된 성능을 발휘할 수 없다.

인스톨이 완료된후에, 카우치베이스 웹 콘솔을 열어 보면, 다음과 같이 인스톨된 카우치베이스 서버의 상태를 볼 수 있다. 

 




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. ㅇㅇ; 2016.06.09 10:37  댓글주소  수정/삭제  댓글쓰기

    헐. 대박... 이게 그동안 왜 안알려졌지...
    카산드라랑 요즘 관심이 많은데 좋은거 얻어갑니다~~

빠르게 훝어보는 node.js

#12 - Socket.IO (4/4)

조대협 (http://bcho.tistory.com)


채팅 프로그램에 (room/그룹) 기능을 추가하기

다음은 앞에서 만든 1:1 귓속말이 가능한 채팅에 채팅방기능을 추가한 버전이다.

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

 

var app = express();

app.use(express.bodyParser());

app.use(express.cookieParser('your secret here'));

app.use(express.session());

app.use(express.static(path.join(__dirname, 'public')));

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.favicon());

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

 

var httpServer =http.createServer(app).listen(3000, function(req,res){

    console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

var count = 0;

var rooms = [];

 

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

 

 

 

io.sockets.on('connection',function(socket){

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

 

        socket.set('room', data.room,function() {

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

 

                // broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                count++;

            });

        });

 

    });

 

    socket.on('changename',function(data){

        socket.get('room',function(err,room){

            socket.get('nickname',function(err,pre_nick) {

                var nickname = data.nickname;

                // if user changes name get previous nickname from nicknames MAP

                if (pre_nick != undefined) {

                    delete rooms[room].socket_ids[pre_nick];

                }

                rooms[room].socket_ids[nickname] = socket.id

                socket.set('nickname',nickname,function() {

                    data = {msg: pre_nick + ' 님이 ' + nickname + '으로 대화명을 변경하셨습니다.'};

                    io.sockets.in(room).emit('broadcast_msg', data);

 

                    // send changed user nickname lists to clients

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            });

 

        });

    });

 

 

    socket.on('disconnect',function(data){

        socket.get('room',function(err,room) {

            if(err) throw err;

            if(room != undefined

                && rooms[room] != undefined){

 

                socket.get('nickname',function(err,nickname) {

                    console.log('nickname ' + nickname + ' has been disconnected');

                    // 여기에 방을 나갔다는 메세지를 broad cast 하기

                    if (nickname != undefined) {

                        if (rooms[room].socket_ids != undefined

                            && rooms[room].socket_ids[nickname] != undefined)

                            delete rooms[room].socket_ids[nickname];

                    }// if

                    data = {msg: nickname + ' 님이 나가셨습니다.'};

 

                    io.sockets.in(room).emit('broadcast_msg', data);

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            }

        }); //get

    });

 

    socket.on('send_msg',function(data){

        socket.get('room',function(err,room) {

            socket.get('nickname',function(err,nickname) {

                console.log('in send msg room is ' + room);

                data.msg = nickname + ' : ' + data.msg;

                if (data.to == 'ALL') socket.broadcast.to(room).emit('broadcast_msg', data); // 자신을 제외하고 다른 클라이언트에게 보냄

                else {

                    // 귓속말

                    socket_id = rooms[room].socket_ids[data.to];

                    if (socket_id != undefined) {

 

                        data.msg = '귓속말 :' + data.msg;

                        io.sockets.socket(socket_id).emit('broadcast_msg', data);

                    }// if

                }

                socket.emit('broadcast_msg', data);

            });

        });

    })

});

코드를 살펴보자

처음에 입장은 http://localhos:3000/{방이름} 으로 하게 된다.

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

그러면 URL에 있는 방이름을 받아서, index.ejs에 있는 UI로 채팅창을 띄워주고 방이름을 parameterindex.ejs에 넘겨준다.

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

클라이언트가 서버에 접속되면 먼저 클라이언트가 join 이벤트를 보내는데, join 이벤트를 받으면, 이때 같이 room 이름으로 현재 소켓을 room 이름의 room join한다.

        socket.set('room', data.room,function() {

다음으로, 해당 소켓이 어느 룸에 있는지 set 명령을 이용하여 socket 저장해놓는다.

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

윗부분이 room 데이터 객체를 생성하는 것인데, 앞의 예제와는 달리, 현재 연결된 클라이언트의 socket.id 이제는 room 단위로 관리를 해야 한다. 그래서 rooms라는 객체를 이용하여, 해당 room 대해서 rooms.room이라는 객체로 만들고, 그리고, room 현재 연결된 클라이언트 socket.id 저장하는 socket_ids 객체를 생성한다.

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

그리고 나서, socket_ids 귓속말 채팅방 예제와 같이 nickname to socket.id 대한 맵핑 정보를 저장한다.

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

// broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users:

Object.keys(rooms[room].socket_ids)});

                count++;

            });

그리고 위와 같이 현재 room 들어 있는 클라이언트들에게만 , 새로운 사용자가 입장했음을 알리고, 사용자 리스트를 업데이트하는 이벤트를 보낸다.

disconnect 대한 부분도 크게 달라진 것이 없다. Socket_ids 객체가 rooms 아래로 들어갔고, 메시지를 보낼때, 귓속말 채팅방 예제가 io.sockets.emit 대신에, room 범위를 지정하는 in() 메서드를 써서 io.sockets.in(room).emit 같이 보내게 된다.

Sendmsg 이벤트 부분도, broadcast하는 부분에서 to를 이용하여 다음과 같이socket.broadcast.to(room).emit 특정 room에 있는 클라이언트에게만 메시지를 보내는 것으로만 변경되었다

아래는 클라이언트쪽의 코드이다. 앞의 예제에서 나온 귓속말이 가능한 대화방과 코드가 거의 동일하다.

<script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

처음에 접속하였을 때, 서버 코드에서 방이름을 URL로부터 읽어서, 그 방이름으로 join 하는 이벤트를 보낸다.

/vies/index.ejs

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

 

<b>Welcome ROOM : <%= room%></b><p>

    Name <input type="text" id="nickname" /> <input type="button" id="changename" value="Change name"/><br>

    To

    <select id="to">

        <option value="ALL">ALL</option>

    </select>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

 

        $('#changename').click(function(){

            socket.emit('changename',{nickname:$('#nickname').val()});

        });

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('send_msg',{to:$('#to').val(),msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

 

        // 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 "To" 리스트를 변경함

        socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

 

        socket.on('broadcast_msg',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

 

다음은 실제 실행 화면이다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 플라시보 2015.10.17 18:26  댓글주소  수정/삭제  댓글쓰기

    관리자의 승인을 기다리고 있는 댓글입니다

  2. 날나리 2016.12.27 16:52  댓글주소  수정/삭제  댓글쓰기

    채팅자료 보던중 제일 잘 된 것 같아요. 앞으로도 많이 부탁드립니다. socket.set, get 이젠 안먹던데요.

  3. 공부중 2017.10.12 17:51  댓글주소  수정/삭제  댓글쓰기

    set, get이 안먹습니다. 어떻게 해야할까요..

  4. 한련화 2019.05.15 12:20  댓글주소  수정/삭제  댓글쓰기

    위의 샘플 예제를 실행시켜도 안되는데 어떻게 해야 실행이 되나요????
    app.js 내에는 index.html을 호출하는 곳이 없는데???? 어떻게 index.html이 실행이 되는지요??
    아시는 분 설명좀 부탁드립니다. ㅠㅠ