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


Archive»


 
 

딥러닝을 이용한 숫자 이미지 인식 #1/2


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


지난 글(http://bcho.tistory.com/1154 ) 을 통해서 소프트맥스 회귀를 통해서, 숫자를 인식하는 모델을 만들어서 학습 시켜 봤다.

이번글에서는 소프트맥스보다 정확성이 높은 컨볼루셔널 네트워크를 이용해서 숫자 이미지를 인식하는 모델을 만들어 보겠다.


이 글의 목적은 CNN 자체의 설명이나, 수학적 이론에 대한 이해가 목적이 아니다. 최소한의 수학적 지식만 가지고, CNN 네트워크 모델을 텐서플로우로 구현하는데에 그 목적을 둔다. CNN을 이해하기 위해서는 Softmax 등의 함수를 이해하는게 좋기 때문에 가급적이면 http://bcho.tistory.com/1154 예제를 먼저 보고 이 문서를 보는게 좋다. 그 다음에 CNN 모델에 대한 개념적인 이해를 위해서 http://bcho.tistory.com/1149  문서를 참고하고 이 문서를 보는 것이 좋다.


이번 글은 CNN을 적용하는 것 이외에, 다음과 같은 몇가지 팁을 추가로 소개한다.

  • 학습이 된 모델을 저장하고 다시 로딩 하는 방법

  • 학습된 모델을 이용하여 실제로 주피터 노트북에서 글씨를 써보고 인식하는 방법

MNIST CNN 모델


우리가 만들고자 하는 모델은 두개의 컨볼루셔널 레이어(Convolutional layer)과, 마지막에 풀리 커넥티드 레이어 (fully connected layer)을 가지고 있는 컨볼루셔널 네트워크 모델(CNN) 이다.

모델의 모양을 그려보면 다음과 같다.


입력 데이타

입력으로 사용되는 데이타는 앞의 소프트맥스 예제에서 사용한 데이타와 동일한 손으로 쓴 숫자들이다. 각 숫자 이미지는 28x28 픽셀로 되어 있고, 흑백이미지이기 때문에 데이타는 28x28x1 행렬이 된다. (만약에 칼라 RGB라면 28x28x3이 된다.)

컨볼루셔널 계층

총 두 개의 컨볼루셔널 계층을 사용했으며, 각 계층에서 컨볼루셔널 필터를 사용해서, 특징을 추출한다음에, 액티베이션 함수 (Activation function)으로, ReLu를 적용한 후, 맥스풀링 (Max Pooling)을 이용하여, 주요 특징을 정리해낸다.

이와 같은 컨볼루셔널 필터를 두개를 중첩하여 적용하였다.

마지막 풀리 커넥티드 계층

컨볼루셔널 필터를 통해서 추출된 특징은 풀리 커넥티드 레이어(Fully connected layer)에 의해서 분류 되는데, 풀리 커넥티드 레이어는 하나의 뉴럴 네트워크를 사용하고, 그 뒤에 드롭아웃 (Dropout) 계층을 넣어서, 오버피팅(Overfitting)이 발생하는 것을 방지한다.  마지막으로 소프트맥스 (Softmax) 함수를 이용하여 0~9 열개의 숫자로 분류를 한다.


학습(트레이닝) 코드

이를 구현하기 위한 코드는 다음과 같다.


코드

import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data



tf.reset_default_graph()


np.random.seed(20160704)

tf.set_random_seed(20160704)


# load data

mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)


# define first layer

num_filters1 = 32


x = tf.placeholder(tf.float32, [None, 784])

x_image = tf.reshape(x, [-1,28,28,1])


W_conv1 = tf.Variable(tf.truncated_normal([5,5,1,num_filters1],

                                         stddev=0.1))

h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')


b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))

h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)


h_pool1 = tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


# define second layer

num_filters2 = 64


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2,

                      strides=[1,1,1,1], padding='SAME')


b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


# define fully connected layer

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


num_units1 = 7*7*num_filters2

num_units2 = 1024


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))

hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


#define loss (cost) function

t = tf.placeholder(tf.float32, [None, 10])

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(k,t))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()


# start training

i = 0

for _ in range(1000):

   i += 1

   batch_xs, batch_ts = mnist.train.next_batch(50)

   sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})

   if i % 500 == 0:

       loss_vals, acc_vals = [], []

       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)

       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))


saver.save(sess, 'cnn_session')

sess.close()



데이타 로딩 파트

그러면 코드를 하나씩 살펴보도록 하자.

맨 처음 블럭은 데이타를 로딩하고 각종 변수를 초기화 하는 부분이다.

import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data


#Call tf.reset_default_graph() before you build your model (and the Saver). This will ensure that the variables get the names you intended, but it will invalidate previously-created graphs.


tf.reset_default_graph()


np.random.seed(20160704)

tf.set_random_seed(20160704)


# load data

mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)


Input_data 는 텐서플로우에 내장되어 있는 MNIST (손으로 쓴 숫자 데이타)셋으로, read_data_sets 메서드를 이요하여 데이타를 읽었다. 데이타 로딩 부분은 앞의 소프트맥스 MNIST와 같으니 참고하기 바란다.


여기서 특히 주목해야 할 부분은 tf.reset_default_graph()  인데, 주피터 노트북과 같은 환경에서 실행을 하게 되면, 주피터 커널을 리스타트하지 않는 이상 변수들의 컨택스트가 그대로 유지 되기 때문에, 위의 코드를 같은 커널에서 tf.reset_default_graph() 없이, 두 번 이상 실행하게 되면 에러가 난다. 그 이유는 텐서플로우 그래프를 만들어놓고, 그 그래프가 지워지지 않은 상태에서 다시 같은 그래프를 생성하면서 나오는 에러인데, tf.reset_default_graph() 메서드는 기존에 생성된 디폴트 그래프를 모두 삭제해서 그래프가 중복되는 것을 막아준다. 일반적인 파이썬 코드에서는 크게 문제가 없지만, 컨택스트가 계속 유지되는 주피터 노트북 같은 경우에는 발생할 수 있는 문제이니, 반드시 디폴트 그래프를 리셋해주도록 하자

첫번째 컨볼루셔널 계층

필터의 정의

다음은 첫번째 컨볼루셔널 계층을 정의 한다. 컨볼루셔널 계층을 이해하려면 컨볼루셔널 필터에 대한 개념을 이해해야 하는데, 다시 한번 되짚어 보자.

컨볼루셔널 계층에서 하는 일은 입력 데이타에 필터를 적용하여, 특징을 추출해 낸다.


이 예제에서 입력 받는 이미지 데이타는  28x28x1 행렬로 표현된 흑백 숫자 이미지이고, 예제 코드에서는 5x5x1 사이즈의 필터를 적용한다.

5x5x1 사이즈의 필터 32개를 적용하여, 총 32개의 특징을 추출할것이다.


코드

필터 정의 부분까지 코드로 살펴보면 다음과 같다.

# define first layer

num_filters1 = 32


x = tf.placeholder(tf.float32, [None, 784])

x_image = tf.reshape(x, [-1,28,28,1])


W_conv1 = tf.Variable(tf.truncated_normal([5,5,1,num_filters1],


x는 입력되는 이미지 데이타로, 2차원 행렬(28x28)이 아니라, 1차원 벡터(784)로 되어 있고, 데이타의 수는 무제한으로 정의하지 않았다. 그래서 placeholder정의에서 shape이 [None,784] 로 정의 되어 있다.  

예제에서는 연산을 편하게 하기 위해서 2차원 행렬을 사용할것이기 때문에, 784 1차원 벡터를 28x28x1 행렬로 변환을 해준다.

x_image는 784x무한개인 이미지 데이타 x를 , (28x28x1)이미지의 무한개 행렬로  reshape를 이용하여 변경하였다. [-1,28,28,1]은 28x28x1 행렬을 무한개(-1)로 정의하였다.


필터를 정의하는데, 필터는 앞서 설명한것과 같이 5x5x1 필터를 사용할것이고, 필터의 수는 32개이기 때문에, 필터 W_conv1의 차원(shape)은 [5,5,1,32] 가된다. (코드에서 32는 num_filters1 이라는 변수에 저장하여 사용하였다.) 그리고 W_conv1의 초기값은 [5,5,1,32] 차원을 가지는 난수를 생성하도록 tf.truncated_normal을 사용해서 임의의 수가 지정되도록 하였다.

필터 적용

필터를 정의했으면 필터를 입력 데이타(이미지)에 적용한다.


h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')


필터를 적용하는 방법은 tf.nn.conv2d를 이용하면 되는데, 28x28x1 사이즈의 입력 데이타인 x_image에 앞에서 정의한 필터 W_conv1을 적용하였다.

스트라이드 (Strides)

필터는 이미지의 좌측 상단 부터 아래 그림과 같이 일정한 간격으로 이동하면서 적용된다.


이를 개념적으로 표현하면 다음과 같은 모양이 된다.


이렇게 필터를 움직이는 간격을 스트라이드 (Stride)라고 한다.

예제에서는 우측으로 한칸 그리고 끝까지 이동하면 아래로 한칸을 이동하도록 각각 가로와 세로의 스트라이드 값을 1로 세팅하였다.

코드에서 보면

h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')

에서 strides=[1,1,1,1] 로 정의한것을 볼 수 있다. 맨앞과 맨뒤는 통상적으로 1을 쓰고, 두번째 1은 가로 스트라이드 값, 그리고 세번째 1은 세로 스트라이드 값이 된다.

패딩 (Padding)

위의 그림과 같이 필터를 적용하여 추출된 특징 행렬은 원래 입력된 이미지 보다 작게 된다.

연속해서 필터를 이런 방식으로 적용하다 보면 필터링 된 특징들이  작아지게되는데, 만약에 특징을  다 추출하기 전에 특징들이 의도하지 않게 유실되는 것을 막기 위해서 패딩이라는 것을 사용한다.


패딩이란, 입력된 데이타 행렬 주위로, 무의미한 값을 감싸서 원본 데이타의 크기를 크게 해서, 필터를 거치고 나온 특징 행렬의 크기가 작아지는 것을 방지한다.

또한 무의미한 값을 넣음으로써, 오버피팅이 발생하는 것을 방지할 수 있다. 코드상에서 padding 변수를 이용하여 패딩 방법을 정의하였다.


h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')



padding=’SAME’을 주게 되면, 텐서플로우가 자동으로 패딩을 삽입하여 입력값과 출력값 (특징 행렬)의 크기가 같도록 한다. padding=’VALID’를 주게 되면, 패딩을 적용하지 않고 필터를 적용하여 출력값 (특징 행렬)의 크기가 작아진다.

활성함수 (Activation function)의 적용

필터 적용이 끝났으면, 이 필터링된 값에 활성함수를 적용한다. 컨볼루셔널 네트워크에서 일반적으로 사용하는 활성함수는 ReLu 함수이다.


코드

b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))

h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)


먼저 bias 값( y=WX+b 에서 b)인 b_conv1을 정의하고, tf.nn.relu를 이용하여, 필터된 결과(h_conv1)에 bias 값을 더한 값을 ReLu 함수로 적용하였다.

Max Pooling

추출된 특징 모두를 가지고 특징을 판단할 필요가 없이, 일부 특징만을 가지고도 특징을 판단할 수 있다. 즉 예를 들어서 고해상도의 큰 사진을 가지고도 어떤 물체를 식별할 수 있지만, 작은 사진을 가지고도 물체를 식별할 수 있다. 이렇게 특징의 수를 줄이는 방법을 서브샘플링 (sub sampling)이라고 하는데, 서브샘플링을 해서 전체 특징의 수를 의도적으로 줄이는 이유는 데이타의 크기를 줄이기 때문에, 컴퓨팅 파워를 절약할 수 있고, 데이타가 줄어드는 과정에서 데이타가 유실이 되기 때문에, 오버 피팅을 방지할 수 있다.


이러한 서브 샘플링에는 여러가지 방법이 있지만 예제에서는 맥스 풀링 (max pooling)이라는 방법을 사용했는데, 맥스 풀링은 풀링 사이즈 (mxn)로 입력데이타를 나눈후 그 중에서 가장 큰 값만을 대표값으로 추출하는 것이다.


아래 그림을 보면 원본 데이타에서 2x2 사이즈로 맥스 풀링을 해서 결과를 각 셀별로 최대값을 뽑아내었고, 이 셀을 가로 2칸씩 그리고 그다음에는 세로로 2칸씩 이동하는 stride 값을 적용하였다.


코드

h_pool1 = tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


Max pooling은 tf.nn.max_pool이라는 함수를 이용해서 적용할 수 있는데, 첫번째 인자는 활성화 함수 ReLu를 적용하고 나온 결과 값인 h_conv1_cutoff 이고, 두 번째 인자인 ksize는 풀링 필터의 사이즈로 [1,2,2,1]은 2x2 크기로 묶어서 풀링을 한다는 의미이다.


다음 stride는 컨볼루셔널 필터 적용과 마찬가지로 풀링 필터를 가로와 세로로 얼마만큼씩 움직일 것인데, strides=[1,2,2,1]로, 가로로 2칸, 세로로 2칸씩 움직이도록 정의하였다.


행렬의 차원 변환

텐서플로우를 이용해서 CNN을 만들때 각각 개별의 알고리즘을 이해할 필요는 없지만 각 계층을 추가하거나 연결하기 위해서는 행렬의 차원이 어떻게 바뀌는지는 이해해야 한다.

다음 그림을 보자


첫번째 컨볼루셔널 계층은 위의 그림과 같이, 처음에 28x28x1 의 이미지가 들어가면 32개의 컨볼루셔널 필터 W를 적용하게 되고, 각각은 28x28x1의 결과 행렬을 만들어낸다. 컨볼루셔널 필터를 거치게 되면 결과 행렬의 크기는 작아져야 정상이지만, 결과 행렬의 크기를 입력 행렬의 크기와 동일하게 유지하도록 padding=’SAME’으로 설정하였다.

다음으로 bias 값 b를 더한후 (위의 그림에는 생략하였다) 에 이 값에 액티베이션 함수 ReLu를 적용하고 나면 행렬 크기에 변화 없이 28x28x1 행렬 32개가 나온다. 이 각각의 행렬에 size가 2x2이고, stride가 2인 맥스풀링 필터를 적용하게 되면 각각의 행렬의 크기가 반으로 줄어들어 14x14x1 행렬 32개가 리턴된다.


두번째 컨볼루셔널 계층


이제 두번째 컨볼루셔널 계층을 살펴보자. 첫번째 컨볼루셔널 계층과 다를 것이 없다.


코드

# define second layer

num_filters2 = 64


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2,

                      strides=[1,1,1,1], padding='SAME')


b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


단 필터값인 W_conv2의 차원이 [5,5,32,64] ([5,5,num_filters1,num_filters2] 부분 )로 변경되었다.


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))


필터의 사이즈가 5x5이고, 입력되는 값이 32개이기 때문에, 32가 들어가고, 총 64개의 필터를 적용하기 때문에 마지막 부분이 64가 된다.

첫번째 필터와 똑같이 stride를 1,1을 줘서 가로,세로로 각각 1씩 움직이고, padding=’SAME’으로 입력과 출력 사이즈를 같게 하였다.


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


맥스풀링 역시 첫번째 필터와 마찬가지로 2,2 사이즈의 필터(ksize=[1,2,2,1]) 를 적용하고 stride값을 2,2로 줘서 (strides=[1,2,2,1]) 가로 세로로 두칸씩 움직이게 하여 결과의 크기가 반으로 줄어들게 하였다.


14x14 크기의 입력값 32개가 들어가서, 7x7 크기의 행렬 64개가 리턴된다.

풀리 커넥티드 계층

두개의 컨볼루셔널 계층을 통해서 특징을 뽑아냈으면, 이 특징을 가지고 입력된 이미지가 0~9 중 어느 숫자인지를 풀리 커넥티드 계층 (Fully connected layer)를 통해서 판단한다.


코드

# define fully connected layer

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


num_units1 = 7*7*num_filters2

num_units2 = 1024


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))

hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


입력된 64개의 7x7 행렬을 1차원 행렬로 변환한다.


h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


다음으로 풀리 커넥티드 레이어에 넣는데, 이때 입력값은 64x7x7 개의 벡터 값을 1024개의 뉴런을 이용하여 학습한다.


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))


그래서 w2의 값은 [num_units1,num_units2]로 num_units1은 64x7x7 로 입력값의 수를, num_unit2는 뉴런의 수를 나타낸다. 다음 아래와 같이 이 뉴런으로 계산을 한 후 액티베이션 함수 ReLu를 적용한다.


hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


다음 레이어에서는 드롭 아웃을 정의하는데, 드롭 아웃은 오버피팅(과적합)을 막기 위한 계층으로, 원리는 다음 그림과 같이 몇몇 노드간의 연결을 끊어서 학습된 데이타가 도달하지 않도록 하여서 오버피팅이 발생하는 것을 방지하는 기법이다.


출처 : http://cs231n.github.io/neural-networks-2/


텐서 플로우에서 드롭 아웃을 적용하는 것은 매우 간단하다. 아래 코드와 같이 tf.nn.dropout 이라는 함수를 이용하여, 앞의 네트워크에서 전달된 값 (hidden2)를 넣고 keep_prob에, 연결 비율을 넣으면 된다.

keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


연결 비율이란 네트워크가 전체가 다 연결되어 있으면 1.0, 만약에 50%를 드롭아웃 시키면 0.5 식으로 입력한다.

드롭 아웃이 끝난후에는 결과를 가지고 소프트맥스 함수를 이용하여 10개의 카테고리로 분류한다.


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)

비용 함수 정의

여기까지 모델 정의가 끝났다. 이제 이 모델을 학습 시키기 위해서 비용함수(코스트 함수)를 정의해보자.

코스트 함수는 크로스엔트로피 함수를 이용한다.

#define loss (cost) function

t = tf.placeholder(tf.float32, [None, 10])

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(k,t))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)


k는 앞의 모델에 의해서 앞의 모델에서

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


으로 softmax를 적용하기 전의 값이다.  Tf.nn.softmax_cross_entropy_with_logits 는 softmax가 포함되어 있는 함수이기 때문에, p를 적용하게 되면 softmax 함수가 중첩 적용되기 때문에, softmax 적용전의 값인 k 를 넣었다.


WARNING: This op expects unscaled logits, since it performs a softmax on logits internally for efficiency. Do not call this op with the output of softmax, as it will produce incorrect results

https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/api_docs/python/functions_and_classes/shard7/tf.nn.softmax_cross_entropy_with_logits.md


t는 플레이스 홀더로 정의하였는데, 나중에 학습 데이타 셋에서 읽을 라벨 (그 그림이 0..9 중 어느 숫자인지)이다.


그리고 이 비용 함수를 최적화 하기 위해서 최적화 함수 AdamOptimizer를 사용하였다.

(앞의 소프트맥스 예제에서는 GradientOptimizer를 사용하였는데, 일반적으로 AdamOptimizer가 좀 더 무난하다.)

학습

이제 모델 정의와, 모델의 비용함수와 최적화 함수까지 다 정의하였다. 그러면 이 그래프들을 데이타를 넣어서 학습 시켜보자.  학습은 배치 트레이닝을 이용할것이다.


학습 도중 학습의 진행상황을 보기 위해서 학습된 모델을 중간중간 테스트할것이다. 테스트할때마다 학습의 정확도를 측정하여 출력하는데, 이를 위해서 정확도를 계산하는 함수를 아래와 같이 정의한다.


#define validation function

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


correct_prediction은 학습 결과와 입력된 라벨(정답)을 비교하여 맞았는지 틀렸는지를 리턴한다.

argmax는 인자에서 가장 큰 값의 인덱스를 리턴하는데, 0~9 배열이 들어가 있기 때문에 가장 큰 값이 학습에 의해 예측된 숫자이다. p는 예측에 의한 결과 값이고, t는 라벨 값이다 이 두 값을 비교하여 가장 큰 값이 있는 인덱스가 일치하면 예측이 성공한것이다.

correct_pediction은 bool 값이기 때문에, 이 값을 숫자로 바꾸기 위해서 tf.reduce_mean을 사용하여, accuracy에 저장하였다.


이제 학습을 세션을 시작하고, 변수들을 초기화 한다.

# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()


다음 배치 학습을 시작한다.

# start training

i = 0

for _ in range(10000):

   i += 1

   batch_xs, batch_ts = mnist.train.next_batch(50)

   sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})

   if i % 500 == 0:

       loss_vals, acc_vals = [], []

       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)

       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))


학습은 10,000번 루프를 돌면서 한번에 50개씩 배치로 데이타를 읽어서 학습을 진행하고, 500 번째 마다 중각 학습 결과를 출력한다. 중간 학습 결과에서는 10,000 중 몇번째 학습인지와, 비용값 그리고 정확도를 출력해준다.


코드를 보자


   batch_xs, batch_ts = mnist.train.next_batch(50)


MNIST 학습용 데이타 셋에서 50개 단위로 데이타를 읽는다. batch_xs에는 학습에 사용할 28x28x1 사이즈의 이미지와, batch_ts에는 그 이미지에 대한 라벨 (0..9중 어떤 수인지) 가 들어 있다.

읽은 데이타를 feed_dict를 통해서 피딩(입력)하고 트레이닝 세션을 시작한다.


  sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})


이때 마지막 인자에 keep_prob를 0.5로 피딩하는 것을 볼 수 있는데, keep_prob는 앞의 드롭아웃 계층에서 정의한 변수로 드롭아웃을 거치지 않을 비율을 정의한다. 여기서는 0.5 즉 50%의 네트워크를 인위적으로 끊도록 하였다.


배치로 학습을 진행하다가 500번 마다 중간중간 정확도와 학습 비용을 계산하여 출력한다.

   if i % 500 == 0:

       loss_vals, acc_vals = [], []


여기서 주목할 점은 아래 코드 처럼 한번에 검증을 하지 않고 테스트 데이타를 4등분 한후, 1/4씩 테스트 데이타를 로딩해서 학습비용(loss)와 학습 정확도(accuracy)를 계산하는 것을 볼 수 있다.


       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)


이유는 한꺼번에 많은 데이타를 로딩해서 검증을 할 경우 메모리 문제가 생길 수 있기 때문에, 4번에 나눠 걸쳐서 읽고 검증한 다음에 아래와 같이 학습 비용은 4번의 학습 비용을 합하고, 정확도는 4번의 학습 정확도를 평균으로 내어 출력하였다.


       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))

학습 결과 저장

학습을 통해서 최적의 W와 b값을 구했으면 이 값을 예측에 이용해야 하는데, W 값들이 많고, 이를 일일이 출력해서 파일로 저장하는 것도 번거롭고 해서, 텐서플로우에서는 학습된 모델을 저장할 수 있는 기능을 제공한다. 학습을 통해서 계산된 모든 변수 값을 저장할 수 있는데,  앞에서 세션을 생성할때 생성한 Saver (saver = tf.train.Saver())를 이용하면 현재 학습 세션을  저장할 수 있다.


코드

saver.save(sess, 'cnn_session')

sess.close()


이렇게 하면 현재 디렉토리에 cnn_session* 형태의 파일로 학습된 세션 값들이 저장된다.

그래서 추후 예측을 할때 다시 학습할 필요 없이 이 파일을 로딩해서, 모델의 값들을 복귀한 후에, 예측을 할 수 있다. 이 파일을 읽어서 예측을 하는 것은 다음글에서 다루기로 한다.


예제 코드 : https://github.com/bwcho75/tensorflowML/blob/master/MNIST_CNN_Training.ipynb


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License


구글 데이타 스트리밍 데이타 분석 플랫폼 dataflow - #1 소개


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


실시간 데이타 처리에서는 들어오는 데이타를 바로 읽어서 처리 하는 스트리밍 프레임웍이 대세인데, 대표적인 프레임웍으로는 Aapche Spark등을 들 수 있다. 구글의 DataFlow는 구글 내부의 스트리밍 프레임웍을 Apache Beam이라는 형태의 오픈소스로 공개하고 이를 실행하기 위한 런타임을 구글 클라우드의 DataFlow라는 이름으로 제공하고 있는 서비스이다.


스트리밍 프레임웍 중에서 Apache Spark 보다 한 단계 앞선 개념을 가지고 있는 다음 세대의 스트리밍 프레임웍으로 생각할 수 있다. Apache Flink 역시 유사한 개념을 가지면서 Apache Spark의 다음 세대로 소개 되는데, 이번글에서는 이 DataFlow에 대한 전체적인 개념과 프로그래밍 모델등에 대해서 설명하고자 한다.  스트리밍 데이타 처리에 대한 개념은 http://bcho.tistory.com/1119 글을 참고하기 바란다.

소개

dataflow에 대해서 이해하기 위해서 프로그래밍 모델을 먼저 이해해야 하는데, dataflow의 프로그래밍 모델은 얼마전에 Apache에 Beam이라는 오픈 소스 프로젝트로 기증 되었다. Apache Spark이나, Apache Flink와 유사한 스트리밍 처리 프레임웍이라고 생각하면 된다. dataflow는 이 Apache beam의 프로그래밍 모델을 실행할 수 있는 런타임 엔진이라고 생각하면 된다. 예를 들어 Apache beam으로 짠 코드를 Servlet이나 Spring 코드라고 생각하면, dataflow는 이를 실행하기 위한 Tomcat,Jetty,JBoss와 같은 런타임의 개념이다.


런타임

Apache Beam으로 작성된 코드는 여러개의 런타임에서 동작할 수 있다. 구글 클라우드의 Dataflow 서비스에서 돌릴 수 도 있고, Apache Flink나 Apache Spark 클러스터 위에서도 그 코드를 실행할 수 있으며, 로컬에서는 Direct Pipeline이라는 Runner를 이용해서 실행이 가능하다.


여러 런타임이 있지만 구글 클라우드의 Dataflow 런타임을 사용하면 다음과 같은 장점이 있다.


매니지드 서비스로 설정과 운영이 필요 없다.

스트리밍 처리는 하나의 노드에서 수행되는 것이 아니라, 여러개의 노드에서 동시에 수행이 되기 때문에, 이 환경을 설치하고 유지 보수 하는 것만 해도 많은 노력이 들지만, Dataflow는 클라우드 서비스이기 때문에 별도의 설치나 운영이 필요없고, 작성한 코드를 올려서 실행 하기만 하면 된다.

Apache Spark등을 운영해본 사람들은 알겠지만, Spark 코드를 만드는 것 이외에도, Spark 클러스터를 설치하고 운영 하는 것 자체가 일이기 때문에, 개발에 집중할 시간이 줄어든다.

오토 스케일링을 지원하기 때문에, 필요한 만큼 컴퓨팅 자원을 끌어다가 빠르게 연산을 끝낼 수 있다.

클라우드 컴퓨팅의 장점은 무한한 자원을 이용하여, 워크로드에 따라서 자원을 탄력적으로 배치가 가능한 것인데, Dataflow 역시, 이러한 클라우드의 장점을 이용하여, 들어오는 데이타량이나 처리 부하에 따라서 자동을 오토 스케일링이 가능하다.


그림처럼 오전에 800 QPS (Query per second)의 처리를 하다가 12시경에 부하가 5000 QPS로 늘어나면 그만한 양의 리소스 (컴퓨팅)를 더 투여해서 늘어나는 부하에 따라서 탄력적으로 대응이 가능하다.

리밸런싱(Rebalancing)기능을 이용하여 작업을 골고루 분배가 가능하다.

Spark이나 Hadoop Map & Reduce와 같은 대용량 분산 처리 시스템의 경우 문제가 특정 노드의 연산이 늦게 끝나서 전체 연산이 늦게 끝나는 경우가 많다. 예를 들어 1000개의 데이타를 10개씩 100개의 노드에서 분산하여 처리를 한후 그 결과를 모두 모아서 합치는 연산이 있다고 할때, 1~2개의 노드가 연산이 늦게 끝나더라도 그 결과가 있어야 전체 값을 합칠 수 있기 때문에, 다른 노드의 연산이 끝나도 다른 노드들은 기다려야 하고 전체 연산 시간이 느려 진다.


Dataflow의 경우는 이런 문제를 해결 하기 위해서, 리밸런싱(rebalancing)이라는 메카니즘을 발생하는데, 위의 그림(좌측의 그래프는 각 노드의 연산 시간이다.) 과 같이 특정 노드의 연산이 느려진 경우, 느려진 노드의 데이타를 다른 연산이 끝난 노드로 나눠서 재 배치하여 아래와 같이 전체 연산 시간을 줄일 수 있다.




저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

대충보는 Storm #3-Storm 싱글 클러스터 노드 설치 및 배포

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

 

지난번에는 간략하게, Storm을 이용한 HelloStorm 애플리케이션을 개발용 클러스터인 Local Cluster에서 구동해봤다. 이번에는 운영용 클러스터를 설정하고, 이 운영 클러스터에 지난번에 작성한 HelloStorm 토폴리지를 배포해보도록 한다.

Storm 클러스터의 기본 구조

Storm 클러스터를 기동하기 전에, 클러스터가 어떤 노드들로 구성이 되는지 먼저 알아보도록 하자 Storm 클러스터는 기본적으로 아래와 같은 3가지 구성요소로 구성이 되어 있다.

먼저 주요 노드인 Nimbus Supervior 노드에 대해서 알아보자, Nimbus Supervisor 노드는 각각 하나의 물리 서버로 생각하면 된다.


Nimbus

Nimbus는 마스터 노드로 주요 설정 정보를 가지고 있으며, Nimbus 노드를 통해서 프로그래밍 된 토폴로지를 Supervisor 노드로 배포한다. 일종의 중앙 컨트롤러로 생각하면 된다. Storm에서는 중앙의 하나의 Nimbus 노드만을 유지한다.

Supervisor

Supervisor 노드는 실제 워커 노드로, Nimbus로 부터 프로그램을 배포 받아서 탑재하고, Nimbus로 부터 배정된 작업을 실행하는 역할을 한다. 하나의 클러스터에는 여러개의 Supervisor 노드를 가질 수 있으며, 이를 통해서 여러개의 서버를 통해서 작업을 분산 처리할 수 있다.

Zookeeper

이렇게 여러개의 Supervisor를 관리하기 위해서, Storm Zookeeper를 통해서 각 노드의 상태를 모니터링 하고, 작업의 상태들을 공유한다.

Zookeeper는 아파치 오픈소스 프로젝트의 하나로, 분산 클러스터의 노드들의 상태를 체크하고 공유 정보를 관리하기 위한 분산 코디네이터 솔루션이다.

전체적인 클러스터의 구조를 살펴보면 다음과 같다.



<그림. Storm 클러스터의 구조>

하나의 싱글 머신에 Nimbus가 설치되고, 다른 각각의 머신에 Supervisor가 하나씩 설치된다. 그리고, ZooKeeper 클러스터를 통해서 기동이 된다.

※ 실제 물리 배포 구조에서는 Nimbus Supervisor, ZooKeeper등을 하나의 서버에 분산배포할 수 도 있고, 여러가지 다양한 배포구조를 취할 수 있으나, Supervisor의 경우에는 하나의 서버에 하나의 Supervisor만을 설치하는 것을 권장한다. Supervisor의 역할이 하나의 물리서버에 대한 작업을 관리하는 역할이기 때문에, 한 서버에 여러 Supervisor를 설치하는 것은 적절하지 않다.

 

설치와 기동

Storm 클러스터를 기동하기 위해서는 앞에서 설명한바와 같이 ZooKeeper가 필요하다. Zookeeper를 다운로드 받은 후에, ~/conf/zoo_sample.cfg 파일을 ~/conf/zoo.cfg로 복사한다.

다음으로 ZooKeeper를 실행한다.

% $ZooKeeper_HOME/bin/zkServer.cmd


<그림. 주키퍼 기동 로드>


다음으로 Nimbus 노드를 실행해보자. Storm을 다운 받은 후 압축을 푼다.

다음 $APACHE_STORM/bin 디렉토리에서

%storm nimbus

를 실행하면 nimbus 노드가 실행된다. 실행 결과나 에러는 $APACHE_STORM/logs 디렉토리에 nimbus.log라는 파일로 기록이 된다.

정상적으로 nimbus 노드가 기동이 되었으면 이번에는 supervisor 노드를 기동한다.

%storm supervisor

Supervisor에 대한 노드는 $APACHE_STORM/logs/supervisor.log 라는 이름으로 기록된다.

Storm은 자체적으로 클러서터를 모니터링 할 수 있는 UI 콘솔을 가지고 있다. UI를 기동하기 위해서는

%storm ui

로 실행을 해주면 UI가 기동이 되며 http://localhost:8080 에 접속을 하면 관리 콘솔을 통해서 현재 storm의 작동 상태를 볼 수 있다.



<그림. Storm UI를 이용한 기동 상태 모니터링>


현재 하나의 PC nimbus supervior,UI를 모두 배포하였기 때문에 다음과 같은 물리적인 토폴로지가 된다.



<그림. 싱글 서버에 nimbus supervisor를 같이 설치한 예>

싱글 클러스터 노드에 배포 하기

싱글 노드 클러스터를 구축했으니, 앞의 1장에서 만든 HelloStorm 토폴로지를 이 클러스터에 배포해보도록 하자.

전장의 예제에서 만든 토폴로지는 Local Cluster를 생성해서, 자체적으로 개발 테스트용 클러스터에 토폴로지를 배포하도록 하는 코드였다면, 이번에는 앞에서 생성한 Storm 클러스터에 배포할 수 있는 토폴로지를 다시 만들어야 한다. 이 코드의 차이는 기존 코드와는 다르게 LocalCluster를 생성하지 않고, 기동중인 클러스터에 HelloTopoloy Submit하도록 한다.

package com.terry.storm.hellostorm;

 

import backtype.storm.Config;

import backtype.storm.StormSubmitter;

import backtype.storm.generated.AlreadyAliveException;

import backtype.storm.generated.InvalidTopologyException;

import backtype.storm.topology.TopologyBuilder;

 

public class HelloTopology {

        public static void main(String args[]){

               TopologyBuilder builder = new TopologyBuilder();

               builder.setSpout("HelloSpout", new HelloSpout(),2);

               builder.setBolt("HelloBolt", new HelloBolt(),4).shuffleGrouping("HelloSpout");

              

               Config conf = new Config();

               // Submit topology to cluster

               try{

                       StormSubmitter.submitTopology(args[0], conf, builder.createTopology());

               }catch(AlreadyAliveException ae){

                       System.out.println(ae);

               }catch(InvalidTopologyException ie){

                       System.out.println(ie);

               }

              

        }

 

}

<그림. HelloTopology.java>


토폴로지 클래스를 만들었으면 이를 빌드해보자

%mvn clean install

을 실행하면 ~/target 디렉토리에 토폴로지 jar가 생성된것을 확인할 수 있다.



jar 파일을 배포해보도록 하자.

배포는 storm {jar} {jar파일명} {토폴로지 클래스명} {토폴로지 이름} 명령을 실행하면 된다.

% storm jar hellostorm-0.0.1-SNAPSHOT.jar com.terry.storm.hellostorm.HelloTopology HelloTopology

배포 명령을 내리면 $APACHE_STORM_HOME/logs/nimbus.log에 다음과 같이 HelloTopology가 배포되는 것을 확인할 수 있다.


2015-01-25T07:35:03.352+0900 b.s.d.nimbus [INFO] Uploading file from client to storm-local\nimbus\inbox/stormjar-8c25c678-23f5-436c-b64e-b354da9a3746.jar

2015-01-25T07:35:03.365+0900 b.s.d.nimbus [INFO] Finished uploading file from client: storm-local\nimbus\inbox/stormjar-8c25c678-23f5-436c-b64e-b354da9a3746.jar

2015-01-25T07:35:03.443+0900 b.s.d.nimbus [INFO] Received topology submission for HelloTopology with conf {"topology.max.task.parallelism" nil, "topology.acker.executors" nil, "topology.kryo.register" nil, "topology.kryo.decorators" (), "topology.name" "HelloTopology", "storm.id" "HelloTopology-1-1422138903"}

2015-01-25T07:35:03.507+0900 b.s.d.nimbus [INFO] Activating HelloTopology: HelloTopology-1-1422138903

2015-01-25T07:35:03.606+0900 b.s.s.EvenScheduler [INFO] Available slots: (["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6702] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6701] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6700])

2015-01-25T07:35:03.652+0900 b.s.d.nimbus [INFO] Setting new assignment for topology id HelloTopology-1-1422138903: #backtype.storm.daemon.common.Assignment{:master-code-dir "storm-local\\nimbus\\stormdist\\HelloTopology-1-1422138903", :node->host {"226ceb74-c1a3-4b1a-aab5-2384e68124c5" "terry-PC"}, :executor->node+port {[3 3] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [6 6] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [5 5] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [4 4] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [7 7] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [2 2] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703], [1 1] ["226ceb74-c1a3-4b1a-aab5-2384e68124c5" 6703]}, :executor->start-time-secs {[7 7] 1422138903, [6 6] 1422138903, [5 5] 1422138903, [4 4] 1422138903, [3 3] 1422138903, [2 2] 1422138903, [1 1] 1422138903}}

2015-01-25T07:37:48.901+0900 b.s.d.nimbus [INFO] Updated HelloTopology-1-1422138903 with status {:type :inactive}

해당 토폴로지가 배포되었는지를 확인하려면 storm list라는 명령어를 사용하면 현재 기동되고 있는 토폴로지 목록을 확인할 수 있다.




<그림. storm list 명령으로 기동중인 토폴로지를 확인>


실행 결과는 $APACHE_STORM_HOME/logs 디렉토리를 보면 worker-xxx.logs 라는 파일이 생긴것을 확인해 볼 수 있는데, 파일 내용을 보면 다음과 같다.

2015-01-25T07:35:08.908+0900 STDIO [INFO] Tuple value ishello world

2015-01-25T07:35:08.908+0900 STDIO [INFO] Tuple value ishello world

우리가 앞서 구현한 Bolt에서 System.out으로 출력한 내용이 출력된다.

동작을 확인하였으면, 이제 기동중인 토폴로지를 정지 시켜 보자. 토폴로지의 정지는 storm deactivate {토폴로지명} 을 사용하면 된다. 아까 배포한 토폴로지 명이 HelloTopology였기 때문에 다음과 같이 토폴로지를 정지 시킨다.

%storm deactivate HelloTopology

그 후에 다시 storm list 명령을 이용해서 토폴로지 동작 상태를 확인해보면 다음과 같다.



< 그림. Storm  토폴로지 정지와 확인 >


만약에 Topology를 재 배포 하려면 storm kill로 해당 토폴로지를 삭제한 후에, 다시 배포 해야 한다. 이때 주의할점은 storm kill로 삭제해도 바로 삭제가 안되고 시간 텀이 있으니 약간 시간을 두고 재 배포를 해야 한다.


지금까지 간단하게, 운영용 클러스터를 구성하고, 운영 클러스터에 토폴로지를 배포 하는 것에 대해서 알아보았다.

HelloStorm 코드 구현, 클러스터 노드 구축 및 배포를 통해서 간단하게 스톰이 무엇을 하는 것인지는 파악했을 것으로 안다. 다음 장에는 조금 더 구체적으로 Storm의 개념과 스톰의 아키텍쳐등에 대해서 살펴보도록 하겠다.


 


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

redis Introduction


Intro
Redis는 "REmote DIctionary System"의 약자로 메모리 기반의 Key/Value Store 이다.
Cassandra나 HBase와 같이 NoSQL DBMS로 분류되기도 하고, memcached와 같은 In memory 솔루션으로 분리되기도 한다.
성능은 memcached에 버금가면서 다양한 데이타 구조체를 지원함으로써 Message Queue, Shared memory, Remote Dictionary 용도로도 사용될 수 있으며, 이런 이유로 인스탄트그램, 네이버 재팬의 LINE 메신져 서비스, StackOverflow,Blizzard,digg 등 여러 소셜 서비스에 널리 사용되고 있다.
BSD 라이센스 기반의 오픈 소스이며 최근 VMWare에 인수되어 계속해서 업그레이드가 되고 있다.
16,000 라인정도의 C 코드로 작성되었으며, 클라이언트 SDK로는
Action Script,C,C#,C++,Clojure,Erlang,Java,Node.js,Objective-C,Perl,PHP,Python,Smalltalk,Tcl등 대부분의 언어를 지원한다. (참고 : http://www.redis.io/clients )

이번 글에서는 Redis란 무엇인지, 그리고 대략적인 내부 구조에 대해서 살펴보도록 한다.

1. Key/Value Store
Redis는 기본적으로 Key/Value Store이다. 특정 키 값에 값을 저장하는 구조로 되어 있고 기본적인 PUT/GET Operation을 지원한다.

단, 이 모든 데이타는 메모리에 저장되고, 이로 인하여 매우 빠른 write/read 속도를 보장한다. 그래서 전체 저장 가능한 데이타 용량은 물리적인 메모리 크기를 넘어설 수 있다. (물론 OS의 disk swapping 영역등을 사용하여 확장은 가능하겠지만 성능이 급격하게 떨어지기 때문에 의미가 없다.)
데이타 억세스는 메모리에서 일어나지만 server restart 와 같이 서버가 내려갔다가 올라오는 상황에 데이타를 저장을 보장하기 위해서 Disk를 persistence store로 사용한다.

2. 다양한 데이타 타입
단순한 메모리 기반의 Key/Value Store라면 이미 memcached가 있지 않은가? 그렇다면 어떤 차이가 있길래 redis가 유행하는 것일까?
redis가 Key/Value Store이기는 하지만 저장되는 Value가 단순한 Object가 아니라 자료구조를 갖기 때문에 큰 차이를 보인다.
redis가 지원하는 데이타 형은 크게 아래와 같이 5가지가 있다.

1) String
일반적인 문자열로 최대 512mbyte 길이 까지 지원한다.
Text 문자열 뿐만 아니라 Integer와 같은 숫자나 JPEG같은 Binary File까지 저장할 수 있다.

2) Set
set은 string의 집합이다. 여러개의 값을 하나의 Value 내에 넣을 수 있다고 생각하면 되며 블로그 포스트의 태깅(Tag)등에 사용될 수 있다.
재미있는 점은 set간의 연산을 지원하는데, 집합인 만큼 교집합, 합집합, 차이(Differences)를 매우 빠른 시간내에 추출할 수 있다.

3) Sorted Set
set 에 "score" 라는 필드가 추가된 데이타 형으로 score는 일종의 "가중치" 정도로 생각하면 된다.
sorted set에서 데이타는 오름 차순으로 내부 정렬되며, 정렬이 되어 있는 만큼 score 값 범위에 따른 쿼리(range query), top rank에 따른 query 등이 가능하다.


4) Hashes
hash는 value내에 field/string value 쌍으로 이루어진 테이블을 저장하는 데이타 구조체이다.
RDBMS에서 PK 1개와 string 필드 하나로 이루어진 테이블이라고 이해하면 된다.


5) List
list는 string들의 집합으로 저장되는 데이타 형태는 set과 유사하지만, 일종의 양방향 Linked List라고 생각하면 된다. List 앞과 뒤에서 PUSH/POP 연산을 이용해서 데이타를 넣거나 뺄 수 있고, 지정된 INDEX 값을 이용하여 지정된 위치에 데이타를 넣거나 뺄 수 있다. 


6) 데이타 구조체 정리
지금까지 간략하게 redis가 지원하는 데이타 구조체들에 대해서 살펴보았다.
redis의 데이타 구조체의 특징을 다시 요약하자면
  • Value가 일반적인 string 뿐만 아니라, set,list,hash와 같은 집합형 데이타 구조를 지원한다.
  • 저장된 데이타에 대한 연산이나 추가 작업 가능하다. (합집합,교집합,RANGE QUERY 등)
  • set은 일종의 집합, sorted set은 오름차순으로 정렬된 집합, hash는 키 기반의 테이블, list는 일종의 링크드 리스트 와 같은 특성을 지니고 있다.
이러한 집합형 데이타 구조 (set,list,hash)등은 redis에서 하나의 키당 총 2^32개의 데이타를 이론적으로 저장할 수 있으나, 최적의 성능을 낼 수 있는 것은 일반적으로 1,000~5,000개 사이로 알려져 있다.

데이타 구조에 따른 저장 구조를 정리해서 하나의 그림에 도식화해보면 다음과 같다.



3. Persistence
앞서도 언급하였듯이, redis는 데이타를 disk에 저장할 수 있다. memcached의 경우 메모리에만 데이타를 저장하기 때문에 서버가 shutdown 된후에 데이타가 유실 되지만, redis는 서버가 shutdown된 후 restart되더라도, disk에 저장해놓은 데이타를 다시 읽어서 메모리에 Loading하기 때문에 데이타 유실되지 않는다.
redis에서는 데이타를 저장하는 방법이 snapshotting 방식과 AOF (Append on file) 두가지가 있다.

1) snapshotting (RDB) 방식
순간적으로 메모리에 있는 내용을 DISK에 전체를 옮겨 담는 방식이다.
SAVE와 BGSAVE 두가지 방식이 있는데,
SAVE는 blocking 방식으로 순간적으로 redis의 모든 동작을 정지시키고, 그때의 snapshot을 disk에 저장한다.
BGSAVE는 non-blocking 방식으로 별도의 process를 띄운후, 명령어 수행 당시의 메모리 snaopshot을 disk에 저장하며, 저장 순간에 redis는 동작을 멈추지 않고 정상적으로 동작한다.
  • 장점 : 메모리의 snapshot을 그대로 뜬 것이기 때문에, 서버 restart시 snapshot만 load하면 되므로 restart 시간이 빠르다.
  • 단점 : snapshot을 추출하는데 시간이 오래 걸리며, snapshot 추출된후 서버가 down되면 snapshot 추출 이후 데이타는 유실된다.
    (백업 시점의 데이타만 유지된다는 이야기)
2) AOF 방식
AOF(Append On File) 방식은 redis의 모든 write/update 연산 자체를 모두 log 파일에 기록하는 형태이다. 서버가 재 시작될때 기록된  write/update operation을 순차적으로 재 실행하여 데이타를 복구한다. operation 이 발생할때 마다 매번 기록하기 때문에, RDB 방식과는 달리 특정 시점이 아니라 항상 현재 시점까지의 로그를 기록할 수 있으며, 기본적으로 non-blocking call이다.
  • 장점 : Log file에 대해서 append만 하기 때문에, log write 속도가 빠르며, 어느 시점에 server가 down되더라도 데이타 유실이 발생하지 않는다.
  • 단점 : 모든 write/update operation에 대해서 log를 남기기 때문에 로그 데이타 양이 RDB 방식에 비해서 과대하게 크며, 복구시 저장된 write/update operation을 다시 replay 하기 때문에 restart속도가 느리다.
3) 권장 사항
RDB와 AOF 방식의 장단점을 상쇠하기 위해서 두가지 방식을 혼용해서 사용하는 것이 바람직한데
주기적으로 snapshot으로 백업하고, 다음 snapshot까지의 저장을 AOF 방식으로 수행한다.
이렇게 하면 서버가 restart될 때 백업된 snapshot을 reload하고, 소량의 AOF 로그만 replay하면 되기 때문에, restart 시간을 절약하고 데이타의 유실을 방지할 수 있다.


4. Pub/Sub Model
redis는 JMS나 IBM MQ 같은 메세징에 활용할 수 있는데, 1:1 형태의 Queue 뿐만 아니라 1:N 형태의 Publish/Subscribe 메세징도 지원한다.(Publish/Subscribe 구조에서 사용되는 Queue를 일반적으로 Topic이라고 한다.)
하나의 Client가 메세지를 Publish하면, 이 Topic에 연결되어 있는 다수의 클라이언트가 메세지를 받을 수 있는 구조이다. (※ Publish/Subscribe 형태의 messaging 에 대해서는 http://en.wikipedia.org/wiki/Pub/sub  를 참고하기 바란다.)


재미있는 것중에 하나는 일반적인 Pub/Sub 시스템의 경우 Subscribe 하는 하나의 Topic에서만 Subscribe하는데 반해서, redis에서는 pattern matching을 통해서 다수의 Topic에서 message 를 subscribe할 수 있다.
예를 들어 topic 이름이 music.pop music,classic 이라는 두개의 Topic이 있을때, "PSUBSCRIBE music.*"라고 하면 두개의 Topic에서 동시에 message를 subscribe할 수 있다.

5. Replication Topology
redis는 NoSQL 계열의 Key/Store Storage인데 반해서 횡적 확장성을 지원하지 않는다.
쉽게 말해서 2.4.15 현재 버전 기준으로는 클러스터링 기능이 없다. (향후 지원 예정)
그래서 확장성(scalability)과 성능에 제약사항이 있는데, 다행이도 Master/Slave 구조의 Replication(복제)를 지원하기 때문에 성능 부분에 있어서는 어느정도 커버가 가능하다.

Master/Slave replication
Master/Slave Replication이란, redis의 master node에 write된 내용을 복제를 통해서 slave node에 복제 하는 것을 정의한다.
1개의 master node는 n개의 slave node를 가질 수 있으며, 각 slave node도 그에 대한 slave node를 또 가질 수 있다.


이 master/slave 간의 복제는 Non-blocking 상태로 이루어진다. 즉 master node에서 write나 query 연산을 하고 있을 때도 background로 slave node에 데이타를 복사하고 있다는 이야기고, 이는 master/slave node간의 데이타 불일치성을 유발할 수 있다는 이야기이기도 하다.
master node에 write한 데이타가 slave node에 복제중이라면 slave node에서 데이타를 조회할 경우 이전의 데이타가 조회될 수 있다.

Query Off Loading을 통한 성능 향상
그러면 이 master/slave replication을 통해서 무엇을 할 수 있냐? 성능을 높일 수 있다. 동시접속자수나 처리 속도를 늘릴 수 있다. (데이타 저장 용량은 늘릴 수 없다.) 이를 위해서 Query Off Loading이라는 기법을 사용하는데
Query Off Loading은 master node는 write only, slave node는 read only 로 사용하는 방법이다.
단지 redis에서만 사용하는 기법이 아니라, Oracle,MySQL과 같은 RDBMS에서도 많이 사용하는 아키텍쳐 패턴이다.
대부분의 DB 트렌젝션은 웹시스템의 경우 write가 10~20%, read가 70~90% 선이기 때문에, read 트렌젝션을 분산 시킨다면, 처리 시간과 속도를 비약적으로 증가 시킬 수 있다. 특히 redis의 경우 value에 대한 여러가지 연산(합집합,교집합,Range Query)등을 수행하기 때문에, 단순 PUT/GET만 하는 NoSQL이나 memcached에 비해서 read에 사용되는 resource의 양이 상대적으로 높기 때문에 redis의 성능을 높이기 위해서 효과적인 방법이다.

Sharding 을 통한 용량 확장
redis가 클러스터링을 통한 확장성을 제공하지 않는다면, 데이타의 용량이 늘어나면 어떤 방법으로 redis를 확장해야 할까?
일반적으로 Sharding이라는 아키텍쳐를 이용한다. Sharding은 Query Off loading과 마친가지로, redis 뿐만 아니라 일반적인 RDBMS나 다른 NoSQL에서도 많이 사용하는 아키텍쳐로 내용 자체는 간단하다.
여러개의 redis 서버를 구성한 후에, 데이타를 일정 구역별로 나눠서 저장하는 것이다. 예를 들어 숫자를 key로 하는 데이타가 있을때 아래와 그림과 같이 redis 서버별로 저장하는 key 대역폭을 정해놓은 후에, 나눠서 저장한다.
데이타 분산에 대한 통제권은 client가 가지며 client에서 애플리케이션 로직으로 처리한다.

현재 버전 2.4.15에서는 Clustering을 지원하지 않아서 Sharding을 사용할 수 밖에 없지만 2012년 내에 Clustering기능이 포함된다고 하니, 확장성에 대해서 기대해볼만하다. redis가 지원할 clustering 아키텍쳐는 ( http://redis.io/presentation/Redis_Cluster.pdf ) 를 참고하기 바란다.

6. Expriation
redis는 데이타에 대해서 생명주기를 정해서 일정 시간이 지나면 자동으로 삭제되게 할 수 있다.
redis가 expire된 데이타를 삭제 하는 정책은 내부적으로 Active와 Passive 두 가지 방법을 사용한다.
Active 방식은 Client가 expired된 데이타에 접근하려고 했을 때, 그때 체크해서 지우는 방법이 있고
Passive 방식은 주기적으로 key들을 random으로 100개만 (전부가 아니라) 스캔해서 지우는 방식이 이다.
expired time이 지난 후 클라이언트에 의해서 접근 되지 않은 데이타는 Active 방식으로 인해서 지워지지 않고 Passive 방식으로 지워져야 하는데, 이 경우 Passive 방식의 경우 전체 데이타를 scan하는 것이 아니기 때문에, redis에는 항상 expired 되었으나 지워지지 않는 garbage 데이타가 존재할 수 있는 원인이 된다.

7. Redis 설치(윈도우즈)
https://github.com/rgl/redis/downloads 에서 최신 버전 다운로드 받은후
redis-server.exe를 실행

클라이언트는 redis-cli.exe를 실행
아래는 테스트 스크립트
    % cd src
    % ./redis-cli
    redis> ping
    PONG
    redis> set foo bar
    OK
    redis> get foo
    "bar"
    redis> incr mycounter
    (integer) 1
    redis> incr mycounter
    (integer) 2
    redis> 


참고 자료


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

대용량 시스템 레퍼런스 디자인


SSAG - Face book Server Side Architecture Group

http://www.facebook.com/groups/serverside

조대협 (bwcho75 골뱅이 지메일닷컴)


I. 배경

웹로직,JBOSS 가 유행이던, J2EE 시대만 하더라도, 웹서버+WAS+RDBMS면 대부분의 업무 시스템을 구현할 수 있었다. 오픈소스가 유행하면서 부터는 프레임웍 수는 다소 많기는 했지만 Spring,IBatis or Hibernate,Struts 정도면 대부분 구현이 가능했다.

그러나 근래 수년 동안 벤더 중심에서 오픈소스 중심에서 기술의 중심이 구글,페이스북이 주도하는 B2C 기반의 서비스의 유행과 더불어 대규모 분산 시스템을 위한 대용량 아키텍쳐가 유행하게 되었는데, 이 아키텍쳐의 특징이 오픈소스 중심에 상당히 다양한 수의 솔루션이 사용 되었다.


II. 내용

이 글에서는 일반적인 대용량 시스템을 구축하기 위한 레퍼런스 아키텍쳐를 소개한다.

일반적인 웹이나 서버 플랫폼을 개발할 수 있는 레퍼런스 아키텍쳐이며, 자주 사용되는 오픈 소스 솔루션을 조합하였다.


또한 이 아키텍쳐는 데이타 분석등의 용도가(OLAP)이 아니라 온라인 트렌젝션 처리 (OLTP)성 업무를 위해서 디자인된 아키텍쳐이다.


III. 레퍼런스 아키텍쳐



1. Reverse Proxy Layer - Routing & Load Balacing

첫번째 계층은 Reverse Proxy 계층으로, 첫번째에 들어오는 HTTP Request에 대한 관문 역할을 한다.

이 Reverse Proxy에서는 초기 Request에 대한 Logging, 필요하다면, Authentication & Authorization 처리를 수행하고, 뒷쪽에 Request를 보낼때, 뒷단의 Node로 Routing 또는 Load Balancing을 한다.

뒷단에 클러스터가 업무에 따라서 여러 클러스터로 나뉠 수 있기 때문에 이에 대한 라우팅을 수행하거나, 같은 업무에 대해서는 단일 클러스터에 대해서 여러 서버에 대한 로드 밸런싱을 수행한다.


뒤에 살아 있는 서버에 대한 리스트나 클러스터에 대한 정보는 ZooKeeper에 저장하며, Scale In/Out시 또는 장애시에는 이 정보를 ZooKeeper에 업데이트 하여, ZooKeeper에 저장된 클러스터 정보를 기준으로 라우팅을 한다.


사용할만한 솔루션으로는 Apache,NginX,HA Proxy등이 있다.

성능 상으로는 NginX가 가능 높다. 그러나 NginX는 대용량 HTTP Request에 대해서 아직 제대로 지원하지 않는 부분이 있다. 파일 업다운 로드의 경우 성능이 급격하게 떨어지는 부분이 있다. 위에서 설명한 ZooKeeper연동이나 Routing 기능들은 module을 구현하여 plug in해야 한다.


2. [Optional] Enterprise Service Bus (ESB) Layer - Cross Cutting Concern, Mash up,Routing,MEP (Message Exchange Pattern Converting),Integration, SLA management,Protocol Converting

이 계층은 필요에 따라 넣거나 뺄 수 있는 Optional 한 부분이다. Enterprise Service Bus는 시스템으로 들어오는 메세지에 대해서 좀 더 확장된(진보된) 기능을 제공하는 계층으로, SOA (Service Oriented Architecture)에서 따온 계층이다. 키 포인트는 성능이다. BY PASS의 경우 10~50ms 이내에 통과(IN/OUT 포함), 몬가 작업을 할 경우에는 100ms 이하에 통과되어야 한다.

ESB 계층에서 다루어야 하는 일들은 다음과 같다.

  • Cross Cutting Concern 처리 : Cross Cutting Concern는 횡종단 처리라고 하는데, 모든 메세지에 대해서 뒷단의 비지니스 로직이 공통적으로 처리해야 하는 부분을 이야기 한다. 대표적인 예로, Logging, Authentication & Authorization 등이 있다. 
  • Routing 처리 : Reverse Proxy 보다 향상된 Routing 기능을 제공할 수 있다. 보통 Reverse Proxy에서는 HTTP Header나 URI등의 최소한의 정보를 바탕으로 라우팅을 하는데 반해서, ESB 계층에서는 Message 본문의 Header나 Message 자체의 내용을 가지고 라우팅을 할 수 있다. 예를 들어 VIP 회원에 대해서는 Dedicated 된 서버로 라우팅을 하는지 등의 처리가 가능하다.
  • Protocol Converting : ESB 계층에서는 또한 Protocol 변환을 할 수 있다. 예를 들어 뒷단의 비지니스 컴포넌트가 XML/REST를 지원하는데, 전체 표준을 JSON/REST를 사용한다면, ESB 계층에 프로토콜 변환 기능을 넣어서 JSON to XML 변환을 수행할 수 도 있고, 또 다른 예로는 통신사의 경우 종종 HTTP 메세지를 손을 대는 경우가 있다. Header에 통신사 고유의 헤더를 삽입하는 등의 일이 있는데, 이런 경우 범용으로 디자인 된 시스템은 
  • Mash Up : Mash Up은 뒤의 비지니스 로직 여러개를 합쳐서 하나의 비지니스 로직을 만들어 내는 것을 이야기 한다. 쉬운 예로 기존 서비스가 "구매" 라는 Function이 있었는데, "포인트 적립" 이라는 기능이 새롭게 추가 되었을때, 비지니스 로직 자체를 변경하는 것이 아니라 기존 "구매" 라는 기능에 +"포인트 적립" 이라는 기능을 Mash up으로 더해서 기능을 변경하는 것이다. 이렇게 하면 비지니스 로직 변화 없이 새로운 기능을 구현할 수 가 있다.
  • MEP Converting : MEP란 Message Exchange Pattern의 약자로 메세지의 호출 방식을 이야기 한다. 쉽게 말하면 Sync,Async 와 같은 호출 방식을 정의하는데, 비지니스 로직이 Long Running 하는 Sync 형식의 서비스 였을 때, ESB를 이용하여, Sync 호출을 Async 형태로 변경할 수 있다. (ESB에서 응답을 먼저 보내고, ESB에서 비지니스 컴포넌트로 ASync로 보내는 형태)
  • Integration : 타 시스템과의 통합을 이야기 한다. 일종의 EAI (Enterprise Application Integration) 기능인데, 앞에서 언급된 Mash up + Protocol Conversion + MEP Converting을 합쳐놓은 기능과도 비슷하다. 크게 대내 시스템간의 통합과 대외 시스템과의 통합등으로 나뉘어지며, 대내 시스템과의 통합은 Legacy (SAP와 같은 ERP, Siebel과 같은 CRM과 같은 패키지 형태의 Application)과 통합 하는 경우가 많으며 이런경우 전용 아답터를 사용하는 경우가 많다. 대외 시스템 통합의 경우 예를 들어 전자 결재나 PUSH 서비스 등과  통합하는 경우이며 이 경우 필요에 따라 프로토콜 변환이나 Authentication & Authorization 처리를 하는 경우가 많으며 특히 과금이 연동되는 경우에는 향후 Audit을 위해서 로그를 기록하고 향후 비교하는 경우가 많다.
  • SLA management : SLA (Service Level Agreement)로, Service의 품질을 보장 하는 기능이다. 정확하게는 SLA를 보장한다기 보다는 SLA에 문제가 생겼을때 이를 빠르게 감지하여 후처리를 할 수 있다. ESB는 시스템으로 들어오는 모든 메세지에 대한 관문 역할을 하기 때문에 응답 시간이나 TPS에 대한 변화가 생겼을때 이를 검출할 수 있는 단일 지점으로, 장애 상황에 대한 검출이 있었을때 이에 대한 후처리를 하도록 관리자에게 통보할 수도 있고, 또는 ZooKeeper를 통해서 성능이 떨어지는 노드들에 대한 Scale Out등을 지시할 수 있다.

ESB는 설계시에 적용을 해놓으면 후에 시스템의 변화가 있을 경우에 도움이 많이 되는 계층이다. 시스템 초기 운영시에는 오히려 큰 이득을 보지 못한다. 왜냐하면 처음에는 모든 비지니스 컴포넌트가 초기 요구 사항에 맞춰서 구현이 되었고, 위의 기능들은 시스템을 운영하면서 요구사항이나 환경적인 변화에 따라 발생하는 요구 사항이기 때문이다. 

앞에서도 언급했으나, ESB는 메세지가 지나가는 중간에 위치 하기 때문에 전체 시스템의 성능에 영향을 주게 되기 때문에 성능에 각별한 신경을 써서 디자인을 해야 하며, 특히 메세지의 파싱하는 과정과 메세지 자체 설계에 신경을 많이 써야 한다. 예를 들어 일반적인 경우 메세지의 BODY 부분을 파싱할 일이 없는데 모든 요청에 따라서 BODY 부분을 파싱하게 한다면 이에 대한 오버로드가 상당히 크게된다.


사용 가능한 솔루션으로는 Apache Mule이나 Oracle사의 Oracle Service Bus등이 있고, 재미있는 장비중의 하나는 Oracle Service Bus 제품중에 XML 기반의 메세징을 파싱하는 부분을 Hardware로 구현해놓은 제품이 있다. Oracle Service Bus도 내부적으로 JAXP 기반의 XML Parser를 이용하는데, 이 구현 부분을 ASIC으로 구현해 놓은 제품이 있는데 이 제품의 경우 메세지 처리 속도를 많이 높일 수 있다.


3. WAS Layer - Business Logic

이 계층은 비지니스 로직을 핸들링 하는 계층이다.

Web Application Server (WAS)로 구현이 가능하며, 고속 멀티플렉싱 기반의 고속 처리가 필요한 경우나 대규모 Stateful Connection이 필요한 경우에는 Netty와 같은 네트워크 서버를 사용한다.

이 계층 구현시 중요한점은 Shared Nothing 아키텍쳐를 적용하는 것을 권장한다.

Shared Nothing이랑 WAS 인스턴스끼리 클러스터링등을 통해서 묶지 않고 각각의 WAS를 독립적으로 돌아가게 설계하는 것이다. (대표적으로 Session Clustering을 사용하지 않는것)

이렇게 하는 이유는 특정 인스턴스가 장애가 났을 때 클러스터를 타고 전파되는 현상을 방지하고 또한 횡적인 확장 (Horizontal Scalability )를 보장하기 위해서이다.

참고 자료 

- WAS 기반의 아키텍쳐 http://bcho.tistory.com/373

- J2EE 그리드 아키텍쳐 http://bcho.tistory.com/330


4. Async message Handling

WAS 계층이 Sync 형태의 동기 (Request/Response) 메세지를 처리한다면, 비동기 메세징 처리나 Publish/Subscribe와 같은 1:N 기반의 비동기 메세지 처리를 하는 계층이 필요하다.

예전에는 MQ나 JMS를 많이 사용했으나, 근래에는 좀더 향상된 프로토콜인 AMQP를 기반으로 한 RabbitMQ가 많이 사용된다.


RabbitMQ의 경우에도 수억명의 사용자를 커버하기에는 클러스터의 확장성이 문제가 있기 때문에 이런 경우에는 MySQL등의 DBMS의 테이블을 큐 처럼 사용하고, 메세지를 읽어가는 부분을 Quartz등을 이용해서 주기적으로 읽어가서 처리하는 구조로 만들게 되면 확장성을 보장할 수 는 있으나, 복잡한 비동기 메세징 (에러처리, Pub/Sub)을 구현하기에는 난이도가 높기 때문에, RabbitMQ를 복수의 클러스터로 묶는 Sharding이나 분산큐(Distributed Queue) 개념을 고려할 필요가 있다.


5. Temporary Storage Layer - Temporary space

다음 계층은 Temporary Storage Layer - 작업 공간이다.

이 작업 공간은 4번의 WAS들이 서로 데이타를 공유할 수 있는 "휘발성", 작업 공간이다. 

필수 조건은 높은 성능을 보장해야 하며, 모든 WAS Node가 접근할 수 있어야 한다. 저장 매체가 Memory냐, Disk냐에 따라서 다음과 같이 나눠볼 수 있다.


1) Data Grid (Memory)

데이타 그리드는 쉽게 생각하면 자바의 HashTable 같은 Key/Value Store 기반의 메모리 Store이다. 단.. 이 그리드는 클러스터 구성을 통해서 용량 확장이 가능하고, 별도의 서버 클러스터로 구성되어 여러개의 WAS 노드들이 접근할 수 있다. 일종의 WAS간의 공유 메모리라고 생각하면 된다.

솔루션으로는 Oracle Coherence (예산만 넉넉하다면 이걸 쓰는게 맘편하다), Redis, memecahed, Terracota 등이 있다.

참고 자료 

- Redis 소개 - http://bcho.tistory.com/654

- Coherence를 활용한 아키텍쳐 설계 - http://bcho.tistory.com/327


2) Working Space (DISK)

트렌젝션을 처리하다 보면, 종종 임시적인 작업 공간이 필요할때가 있다. 예를 들어 드롭 박스와 같은 파일 서비스를 이야기 해보자, 드롭박스는 이미지 파일을 하나 올리면, 모바일 디바이스의 화면 해상도에 맞게 5개의 썸네일 이미지를 재 생산한다. 이런 작업을 하기 위해서는 이미지 파일을 저장하기 위해서 임시로 저장해놨다가 썸네일을 추출하는 공간이 필요한데 이를 임시 작업 공간이라고 한다.

데이타 그리드와 마찬가지로, 여러 노드들이 해당 공간을 공유할 수 있어야 한다. 그래서 NFS (Network File System)이 많이 사용되며, Gluster와 같은 소프트웨어 기반의 NFS나 NetApp社의 NFS appliance server (하드웨어) 등이 있다.

참고 자료

- Amazon에서 Gluster 성능 비교 자료 - http://bcho.tistory.com/645


6. Persistence Layer

다음은 영구 저장 공간이다. 영구 저장 공간은 우리가 일반적으로 생각하는 데이타가 저장되는 공간이라고 보면된다.  쉽게 예를 들 수 있는 공간으로는 데이타 베이스와 파일 시스템을 들 수 있다. 이러한 영구 저장소는 대용량 B2C 시스템의 유행과 함께 새로운 DBMS들이 등장하였는데, DBMS 측면에서는 Key Value Store 기반의 NoSQL이나, 대용량 파일을 저장할 수 있는 Object Store등을 그 예로 들 수 있다.


1) Relational Data

개체간의 관계가 있는 경우에 대한 데이타를 관계형 데이타라고 하고, 이를 핸들링 하기 위해서는 관계형 데이타 베이스 RDBMS를 사용한다. 우리가 지금까지 일반적으로 사용해왔던 데이타 베이스가 이 RDBMS이다. RDBMS는 대용량 서비스를 위해서는 태생적인 한계를 가지고 있는데, 예를 들어 MySQL의 경우 하나의 데이타베이스에서 저장할 수 있는 레코드의 수가 10억개 정도가 최적이다. 

이런 문제를 해결하기 위해서는 대용량 시스템에서 몇가지 기법을 추가로 사용하는데 "Sharding" 과 "Query Off Loading"이다.


Sharding이란, 데이타의 저장용량의 한계를 극복하기 위한 방안으로

데이타를 저장할때 데이타를 여러 데이타 베이스에 걸쳐서 나눠 저장하는 방법이다. 예를 들어 "서울","대구","대전"등 지역별로 데이타베이스를 나눠서 저장하거나(이를 횡분할 Sharding) 또는 10대,20대,30대 식으로 데이타를 나눠서 저장하는 방식(이를 수직분할 Sharding)을 사용한다. 이러한 Sharding은 데이타베이스 계층에서 직접적으로 지원하기가 어렵기 때문에, 애플리케이션 레벨에서 구현해야 한다.


다음으로 Query Off Loading이라는 기법으로, 이 기법은 성능의 한계를 높이기 위한 기법이다. 

"Master DB → Staging DB → Slave DB 1,Slave DB 2,....N"

    1. Create/Update/Write/Delete는 Master DB에서 수행하고
    2. Master DB의 데이타를 Staging DB로 고속 복사한후
    3. Staging DB에서 N개의 Slave DB로 데이타를 복사한다.
    4. Read는 Slave DB에서 수행한다.

일반적인 DBMS 트렌젝션은 10~20% 정도가 Update성이고, 나머지 80~90%가 Read성이기 때문에, Read Node를 분산함으로써, 단일 DBMS 클러스터의 임계 처리 성능을 높일 수 있다.


이때 Master/Staging/Slave DB로 데이타를 복제하는 방식이 매우 중요한데, 여기서 일반적으로 사용하는 방식을 CDC (Change Data Capture)라고 한다.

RDBMS는 데이타 베이스 장애에 대한 복구등을 위해서 모든 트렌젝션을 파일 기반의 로그로 남기는 데 이를 Change Log라고 한다. CDC는 이 Change Log를 타겟 DB에 고속으로 복사해서 다시 수행(Replay)하는 형태로 데이타를 복제한다.


MySQL의 경우 Clustering에서 이 CDC 기능을 기본적으로 제공하고 있고, Oracle의 경우 Oracle Golden Gate라는 솔루션을 이용한다. (비싸다..) 중가격의 제품으로는 Quest의 ShareFlex들을 많이 사용한다.


2) Key/Value Data

다음으로 근래에 들어서 "NoSQL"이라는 간판을 달고 가장 유행하는 기술중의 하나가 Key/Value Store이다.

데이타 구조는 간단하게 Key에 대한 데이타(Value)를 가지고 있는 형태이다. RDBMS와 같이 개체간의 관계를 가지지 않는다.

오로지 대용량,고속 데이타 억세스,데이타에 대한 일관성 에만 초점을 맞춘다. (이중에서 보통 2개에만 집중한다. 이를 CAP 이론 - Consistency, Availability, Performance)


이 기술은 태생 자체가 B2C 서비스를 통해서 탄생하였다.

블로그나 트위터, 페이스북 처럼 데이타의 구조 자체가 복잡하지 않으나 용량이 많고 고성능이 필요한 데이타들이다. 태생 자체가 이렇기 때문에 복잡한 관계(Relationship)을 갖는 복잡한 업무 시스템에는 잘 맞지 않는 경우가 많으며, 트렌젝션 처리나 JOIN, SORTING 등이 어렵기 때문에 애플리케이션의 구현 복잡도가 올라간다.


참고 자료

- 사람들은 왜 NoSQL에 열광하는가? - http://bcho.tistory.com/658

- Amazon Dynamo 계열의 NoSQL 장단점 - http://bcho.tistory.com/622

- NoSQL Riak - http://bcho.tistory.com/621

- NoSQL 계보 정리 - http://bcho.tistory.com/610

- Cassandra 소개 - http://bcho.tistory.com/440


3) Object Data

Object Data는 File과 같이 대용량 데이타 파일 저장을 할 수 있는 Storage이다.

10M,1G와 같은 대용량 파일을 저장할 수 있는 저장소로, Amazon의 S3, Openstack SWIFT등이 대표적인 예이며, 하드웨어 어플라이언스 장비로는 애플의 iCloud로 유명해진 EMC의 isilion등이 있다.

Object Data 저장에 있어서 중요하게 생각하는 부분은 대용량의 데이타를 저장할 수 있는 용량에 대한 확장성과 데이타 저장에 대한 안정성이다. 

이러한 Object Data는 Quorum이라는 개념을 적용하여, 원본을 포함하여 N개의 복사본을  유지한다. 일반적으로는 N+3 (3개의 복사본)을 저장하여 데이타에 대한 안정성을 보장한다. 


4) Document Data

Document Data는 Key/Value Store에서 조금 더 발전한 데이타 저장 방식으로

Key 자체는 동일하나 Value에 해당하는 부분이 Document가 저장된다. Document 는 JSON이나 XML 문서와 같이 구조화된 데이타를 저장한다.

RDBMS가 다양한 select, where, group, sorting,index 등 여러가지 데이타에 대한 기능을 제공한다면, Key/Value Store는 이런 기능은 거의 제공하지 않는다. Document Data를 저장하는 제품들은 RDBMS와 Key/Value Store의 중간정도에서 데이타에 대한 핸들링 기능을 제공한다. (부족한 Indexing 기능, 부족한 Group 기능, 부족한 Sorting 기능등)


대표적은 솔루션으로는 MongoDB,CouchDB, Riak등이 있다.


요즘 들어서 자주 사용되는 대표적인 Persistence Store에 대해서 간단하게나마 집고 넘어갔지만, 사실 이 보다 더 많은 형태의 Persistence Store들과 기능들이 있다.


7. Configuration management & Coordinator

대용량, 분산 시스템으로 발전하면서 풀어야되는 문제중의 하나가 "분산되어 있는 노드들에 대한 설정(Configuration)정보를 어떻게 서로 동기화하고 관리할것인가? (이를 Configuration Management라고 한다.) " 인다. 거기에 더해서 클라우드 인프라를 사용하면서 "전체 클러스터내의 서버들의 상태를 모니터링 해서, 서버의 수를 느리고 줄여야 하며 서버들간의 통신을 중재해야 한다. (이를 Coordination 이라고 한다.)"  


여기에 필요한 기능이 작은 량의 데이타(Configuration Data)를 여러 서버가 공유해서 사용할 수 있어야 하며, 이 데이타의 변화는 양방향으로 클러스터 노드내에 전해져야 한다.

즉 Configuration 정보를 각 서버들이 읽어올 수 있어야 하며, 이 Configuration 정보가 바뀌었을 경우 다른 서버들에게 데이타가 변했음을 통지해줄 수 있어야 하며, 중앙 집중화된 Configuration 정보 뿐만 아니라, 서버의 상태가 변했음을 다른 서버들에게 빠르게 알려줄 수 있어야 한다.


이런 역할을 하는 대표적인 솔루션으로는 ZooKeeper 많이 사용된다.


8. Infrastructure

마지막으로 이런 소프트웨어 스택을 구동하기 위한 하드웨어 인프라가 필요한데, 예전에는 일반적인 서버를 Co-Location이나 Hosting 형태로 사용하는 것이 일반적이었으나, 요즘은 가상화 기술을 기반으로 한 클라우드 (Infrastructure as a service)를 사용하는 경우가 많다.

클라우드의 특징은 "Pay-as-you-go" 로 자원을 사용한 만큼에 대해서만 비용을 지불하는 구조이다. CPU를 사용한 만큼, 디스크를 사용한 만큼, 네트워크 대역폭을 사용한 만큼만 비용을 지불한다.


Amazone WebService (AWS), Microsoft Azure, Google App Engine등이 대표적인 예인데, 이러한 클라우드의 장점은 Time To Market (시장 진입 시간)이 매우 짧다는 것이다. 앉아서 신용카드와 PC만 있다면 인터넷에 접속해서 30분내에 서버,디스크,네트워크등을 설정해서 사용할 수 있다.

단 이러한 클라우드 인프라는 Public 한 서비스 형태로 공유되서 서비스 되기 때문에 일반적인 호스팅과는 달리 성능등에 대한 한계를 가지고 있다. 예를 들어 서버와 디스크간의 네트워크 대역폭이 보장되지 않기 때문에 디스크 IO가 많은 애플리케이션 (DBMS와 같은)에 대한 성능을 보장하기가 쉽지 않고, LAN 설정이 자유롭지 않기 때문에 UDP등을 이용해서 클러스터링을 하는 제품의 경우 클러스터링을 사용할 수 없는 경우가 있다.  이런 이유로, 클라우드위에서 구현되는 시스템의 경우에는 해당 클라우드의 기술적인 특징을 제대로 이해하고 구현해야 한다.


또한 클라우드가 "Pay-as-you-go" 형태로 사용한만큼 비용을 지불한다는 것이 어떻게 보면 "싸다"라고 느껴질 수 있지만, 네트워크,IP 등등 모든 자원에 대해서 비용을 지불하기 때문에 실제적으로 계산해보면 싸지 않은 경우도 많고 기술적인 제약 때문에, 초기 시장 진입을 하는 경우에는 클라우드를 사용하는 경우아 많지만 규모가 커진 서비스의 경우에는 다시 자체 데이타 센타를 구축하는 경우가 많다. (예 소셜 게임 서비스인-Zinga, VOD 서비스인-Netflix)


운영 측면에서 인프라에 대한 관리를 클라우드 업체에 대행시킴으로써 얻는 이득도 있지만 불필요한 비용이 낭비되지 않게 클라우드 인프라에 대한 배포 구조를 끊임 없이 최적화 하는 노력도 필요하다.


IV. 결론

지금까지 현재 유행하는 대용량 고성능 시스템에 대한 레퍼런스 아키텍쳐에 대해서 설명하였다. 사실 이 글을 정리한 이유는 글을 쓰는 본인도 기술이 변화함을 느끼고 있었고, 이에 대한 공부와 개념 정리가 필요하다고 느껴서인데, 확실하게 기술 구조는 변했다. 유행하는 기술도 변했다. 대용량 시스템은 이런 구조로 구현하는게 하나의 모범 답안 (정답이 아니라는 이야기)은 될 수 있으나, 대부분의 IT 시스템은 이런 대용량 아키텍쳐 구조 없이도 WAS + RDBMS 구조만으로도 충분히 구현이 가능하다.

그럼에도 불구하고 이러한 레퍼런스 아키텍쳐에 대한 글을 쓴 이유는 레퍼런스 아키텍쳐를 이해하고, 이런 아키텍쳐가 왜 필요한지 어디에 쓰이는지를 이해한 후에 제대로 적용하기를 바라는 마음에서 정리 하였다. 이러한 대용량 기술은 유용한 기술임에는 분명하지만, 닭잡는데 소잡는 칼을 쓸 필요는 없지 않은가?


누락된 부분

※ node.js 를 이용한 Long Running Connection Service : 예-Push 등을 추가 할것.

※ Map & Reduce를 이용한 분산 처리

※ 데이타 분석을 위한 Hadoop 또는 OLAP성의 처리 아키텍쳐


P.S. 요즘 제 포스팅들이 읽이 어려운가요? 내용은 어떤지요? 피드백을 못 받아서 궁금합니다. 요즘 글들이 축약적인 내용이나 추상적인 개념들을 많이 이야기 하는 것 같아서요. 


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Google의 기술을 이해한다.

근래에 들어서 유행하는 IT 기술은 구글이나 페이스북등의 B2C 서비스 업체를 중심으로 하여 파생된 기술이 그를 이룬다.
클라우드 컴퓨팅, NoSQL, 빅데이타등의 최신기술들 역시 구글이나 페이스북을 원류로 한다.
'이 글에서는 대표적인 B2C 기업인 구글의 서비스의 구조를 통하여 구글의 기술을 이해하고 현재 주류를 이루는 기술에 대한 배경을 이해함으로써 향후 유사 솔루션에 대한 적용 시나리오를 찾는데 도움을 주기 위해서 작성되었다.'

검색엔진의 일반적인 구조
구글은 기본적으로 검색 서비스를 바탕으로 유입자를 통한 광고 수입을 주요 비지니스 모델로 하고 있다.
이메일이나 개인 스토리지 서비스등 많은 서비스들을 가지고는 있지만, 아무래도 그 뿌리는 검색이다.
일반적인 검색 엔진의 구조는 다음과 같다. 
※ 아래 구조는 일반적인 기술을 바탕으로 추론한 내용으로, 실제 구글의 검색 엔진 구조와는 상의할 수 있다.



크게 3 부분으로 구성되는데, 주요 컴포넌트는 다음과 같다.
1. Crawler 
Crawler는 인터넷의 웹 페이지들을 샅샅이 뒤져서 모든 페이지 문서를 읽어와서 저장하는 역할을 한다.
2. Index Engine
Index Engine은 저장된 페이지를 분석하여 단어별로 색인(Index) 을 만드는 역할을 한다.
3. Search Engine
Search Engine은 사용자에 의해서 입력된 검색어를 가지고, Index를 검색하여 검색어가 들어있는 페이지를 찾아내서 그 결과를 리턴한다. 
이 컴포넌트의 유기적은 흐름을 보면 다음과 같다.




1) Crawler는 수집해야되는 URL 리스트를 가지고 여러개의 BOT을 기동하여, 각 페이지의 HTML 페이지를 읽어서 저장한다.
2) Index 서버는 저장된 페이지들을 읽어서 Index를 추출한다.
Index는 단어와 페이지 URL을 맵핑한 일종의 테이블이다.
저장시에는 실제 문서 URL을 저장하지 않고, 일종의 Hash 등을 이용하여 문서 ID를 추출하여 저장한다.
아래 저장 구조는 일종의 예로, 실제로 저장되는 구조는 이보다 훨씬 복잡하다.



3) 사용자에 의해 들어온 검색어는 위의 Index 테이블을 검색하여, 문서 ID 리스트를 추출하고 랭킹 알고리즘등을 적용하여 소팅된 형태로 사용자에게 출력된다.


필요 기술

위의 시나리오를 구현하기 위해서 어떤 기술들이 필요할까?

1) Crawler에서 필요한 기술
Crawling은 Crawling Bot들이 여러 웹사이트를 돌아다니면서 그 페이지를 저장한다.
수백만 사이트에서 페이지를 읽어와서 저장해야 하기 때문에, 다음과 같은 조건의 대규모 파일 시스템이 필요하다.
파일 시스템
- 많은 수의 파일을 저장할 수 있을 것
- Crawling Bot은 오직 읽은 페이지를 저장 즉 Write만 하기 때문에, write에 최적화 되어 있어야 한다.
- 저장된 파일에 대한 update는 발생하지 않는다.

2) Index에서 필요한 기술
파일 시스템
- 하나의 파일을 동시에 여러 Index가 처리할 수 있어야 한다. 파일을 여러개로 나눠서 처리 하기 때문에, Random Access가 지원되어야 한다. 이런 요구 사항에서 나온 파일 시스템이 Google의 파일 시스템인 GFS 이다. 

INDEX 기반의 저장 시스템
- 검색어를 KEY WORD로 하고, 문서를 저장하는 대규모 Key/Value Store가 필요하다. 
대용량 분산 Key/Value 스토어를 구현한 것이 Google BigTable로, 현재 대용량 NoSQL에서 Amazon Dynamo 계열과 함께 크게 양대 산맥을 이루는 기반 기술이다.

3) 분산 Locking, 공유 정보

이런 대규모 분산 시스템 처리에서 필요한 점 중 하나가, 분산되어 있는 리소스(파일)에 대한 억세스를 진행할때, 다른 프로세스가 해당 리소스를 동시에 억세스할 수 없도록 배타적(Exclusive)한 접근을 보장해야 한다.
일반적인 방법이 Locking을 이용하는 방법인데, 분산되어 있는 노드와 클라이언트가 억세스 되고 있는 리소스에 대한 Lock 정보를 공유할 수 있어야 하며, 빠른 속도를 보장해야 한다.

4) 분산 처리 기술

이 시스템의 구조가 주목 받는 점중의 하나는 대규모의 데이타를 저장한다는 점이외에도, 대규모 데이타에 대한 처리(연산)이 가능하다는 것인데, 대표적으로 사용되는 기술이 Map & Reduce  라는 기술이다.
Map & Reduce 기술의 개념은 간단하다
 " 큰 데이타를 여러개의 조각으로 나눠서 여러대의 컴퓨터가 각 조각을 처리하고, 처리된 결과를 모아서 하나의 단일 결과를 낸다."

※ Map & Reduce에 대한 자세한 개념은 - http://bcho.tistory.com/650 글을 참고

실제 구현화된 기술과 레퍼런스 구현

이러한 기술들을 실제로 구현화해 놓은 시스템의 스택 구조와 이 개념을 바탕으로 구현된 오픈 소스들을 살펴보면 다음과 같다.




종류Google오픈소스
분산파일 시스템Google File System (GFS)Apache Hadoop File System (HDFS)
Key/Value 저장Google Big TableApache HBase
분산 처리Google Map & ReduceApache Hadoop
분산 LockingGoogle ChubbyN/A

구글에서 해당 솔루션 구축에 대한 논문을 발표했고, 이 원리를 바탕으로 뛰어난 개발자들이 오프소스에 기여하여, Apache Hadoop 을 필두로 하여, Google 의 시스템 Stack과 유사한 오픈소스가 나왔고, 현재 빅데이타 분석 및 비동기 처리용으로 많이 사용되고 있다. 아쉽지만 아직까지 분산 Locking을 지원하기 위한 Chubby와 같은 솔루션에 대응 되는 솔루션은 없다. ZooKeeper등을 이용하여 분산 Lock 처리를 하거나 애플리케이션에서 이를 구현 및 제어하는 실정이다.

자세하지는 않지만, 
1) 이러한 기술들이 왜 필요하며
2) 어떤 이유에서 만들어 졌으며
3) 어떤 용도로 사용되고 있는지
에 대해서 간단하게 살펴보았다.

앞서 서문에서도 요약하였지만, 이 글의 목적은 구글 기술 자체를 깊이 있게 이해하는 것이 아니라, 너무나 유행이 되버린 분산 처리나, 빅데이타, Hadoop 기술들에 대한 맹신을 없애고, 기술에 대한 제대로된 이해를 바탕으로 적절한 곳에 적절한 기술을 활용하고자 하는데 있다.

참고
1. 분산 파일 저장 시스템 - Apache HDFS 에 대한 소개 : http://bcho.tistory.com/650
2. BigTable 기반의 Key Value Store 구조 - http://bcho.tistory.com/657 (Apache Cassandra의  Local Node당 데이타를 저장하는 구조는 Google의 BigTable과 원리가 같다)

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

E-tailing

  • Recommendation engines — increase average order size by recommending complementary products based on predictive analysis for cross-selling.
  • Cross-channel analytics — sales attribution, average order value, lifetime value (e.g., how many in-store purchases resulted from a particular recommendation, advertisement or promotion).
  • Event analytics — what series of steps (golden path) led to a desired outcome (e.g., purchase, registration).

Financial Services

  • Compliance and regulatory reporting.
  • Risk analysis and management.
  • Fraud detection and security analytics.
  • CRM and customer loyalty programs.
  • Credit scoring and analysis.
  • Trade surveillance.

Government

  • Fraud detection and cybersecurity.
  • Compliance and regulatory analysis.
  • Energy consumption and carbon footprint management.

Health & Life Sciences

  • Campaign and sales program optimization.
  • Brand management.
  • Patient care quality and program analysis.
  • Supply-chain management.
  • Drug discovery and development analysis.

Retail/CPG

  • Merchandizing and market basket analysis.
  • Campaign management and customer loyalty programs.
  • Supply-chain management and analytics.
  • Event- and behavior-based targeting.
  • Market and consumer segmentations.

Telecommunications

  • Revenue assurance and price optimization.
  • Customer churn prevention.
  • Campaign management and customer loyalty.
  • Call Detail Record (CDR) analysis.
  • Network performance and optimization.

Web & Digital Media Services

  • Large-scale clickstream analytics.
  • Ad targeting, analysis, forecasting and optimization.
  • Abuse and click-fraud prevention.
  • Social graph analysis and profile segmentation.
  • Campaign management and loyalty programs.

From http://www.cloudera.com/why-hadoop/

 

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
http://rainblue.kr/1045
트위터에서 재미있는 글을 찾아서 읽다보니, 발상의 전환이 필요하다고 생각되서 정리해 봅니다.
--
국내에는 사용자가 많지 않지만, 트위터보다 많은 트래픽을 자랑하는 마이크로 블로깅 도구입니다. (소셜 네트웤의 넘버 투 라고 자랑하네요.) 하루에 5억 PV, 초당 4만 request, 하루에 3TB 데이터를 저장하는 서비스를 위해 1천대 이상의 서버를 운용하는 텀블러. 4명의 엔지니어가 전형적인 LAMP 스택으로 시작했지만, 지금은 20명의 엔지니어가 점점 성장하는 사이트를 분산환경으로 진화시키고 있다네요
--
초당 4만 TPS, 경이로운 숫자입니다.
처음에는 LAMP로 개발했고 현재는 Scala로 전환중이며, 메인 데이타 베이스는 아직도 MySQL에 Sharding을 사용합니다.

대규모 분산 시스템을 설계할때 요즘은 기본적으로 NoSQL과 분산 기술을 고려합니다. 근데 이런 기술은 솔직하게 사용한 경험이 있는 사람도 많지 않을뿐더러 이는 즉 높은 품질의 서비스를 오픈하기가 어렵다는 이야기 입니다.

그렇다면 대규모 서비스의 정의는?
1. 일단 잘 팔려야 대규모 서비스도 해볼 수 있다.
2. 꼭 분산 환경이나 NoSQL을 써야 한다는 선입관을 바꿀 필요가 있다. → 기존의 MySQL등도 Sharding이나 Query Off-Loading등의 아키텍쳐를 사용하면 가능하다.
3. 나중에 아키텍쳐를 바꾸기 어렵다? → 그렇게 생각했었는데, 위의 사례를 보면 현재 Thumbler도 LAMP에서 Scala로 전환중이고, HBase와 Redis로 서서히 넘어가는 중이고, FaceBook도 예전에는 MySQL이었지만, 다른 NoSQL로 전환하였다. 그것도 운영중에.
결국 대규모 서비스는 서비스가 성공한 다음에 충분한 경험과 예산을 가지고 차차 바꿔 가면 된다.

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

분산 환경 기반의  NoSQL은 예전 포스팅에서도 설명했듯이 크게 Google의 BigTable 논문을 기반으로한 시스템과, Amazon의 Dynamo를 기반으로 한 시스템 두가지로 나뉘어 진다.
Dynamo 계열의 NoSQL의 장단점을 간단히 정리해보면

Dynamo 계열 NoSQL의 개요

1. Ring과 Consistent Hasing
먼저 Dynamo 계열 (Cassandra, Riak) 의 NoSQL의 특징은 Ring 토폴로지를 기본으로 하고 있다. Ring 구성이란, 전체 데이타를 1~N (2^160과 같이 큰 범위로) 이라는 특정 레인지로 정의한후 전체 데이타 저장 구조를 Ring 형으로 정의한 후에, 이 Ring을 피자 조각을 나누듯이 여러 Slice로 나눈다. 이를 Partition이라고 하는데, 각 Partition은 데이타를 저장하는 구간 정보를 가지고 있다. 예를 들어 전체 Ring이 1000개의 데이타를 저장한다고 하고, 각 Partition의 수를 10개로 지정하면 첫번째 파티션은 0~999, 두번째는 1000~1999 까지의 키를 가지는 데이타를 저장한다. 이르 통해서 저장하고자 하는 데이타의 키를 알면 어느 파티션에 저장할 수 있는지 쉽게 찾아갈 수 있기 때문에, 각 Partition을 저장하는 하드웨어(Node)에 부하를 분산 시킬 수 있는 구조를 갖는다. 이런 방식을 Ring 기반의 Consistent Hashing이라고 이야기 한다.

2. N-Value & Quorum
이 경우 특정 파티션을 저장하고 있는 Node가 장애가 났을때, 특성 파티션의 데이타 유실이 발생할 수 있는데, 이를 방지하기 위해서 Node간의 데이타 복제를 수행한다. 몇 개의 복제본을 갖느냐를 정해야 하는데, 이 복제본의 수를 보통 "N-Value" 또는 "Quorum" 이라고 정의하며, 이 Quorum의 수는 일반적으로 3개 정도로 지정한다.
이 N-Value를 3으로 지정하는 이유는 여러가지가 있겠지만, 장애 대응면에서 최소한 하나의 복제본을 가져야하기 때문에 2개의 복제본이 필요하고, 예측된 작업(패치,서버 교체)시에도 장애를 대응하기 위해서 최소한 두개의 복제본을 유지해야 하기 때문에 일반적으로 3개의 복제본을 생성한다. (이 N-Value는 NoSQL 설정에서 조정할 수 있다.)

3. R-Value, W-Value
앞에 설명한 것 처럼, N-Value의 복제본을 가지게 되는데, Dynamo Architecture는 R-Value와 W-Value라는 특성을 유지 한다. 이 값은 "성능과, 데이타 정합성간의 Trade-Off"를 위한 값인데, 데이타 복제는 실시간으로 이루어지지 않는다. 약간의 Delay가 발생한다 (수 밀리세컨드, 데이타 센터간에는 조금더 길 수 있다.)
N-Value를 3이라고 가정하자. 첫번째 Node에 Write를 한후에,  두번째 Node와 세번째 Node에 데이타가 복제 되어야 한다. 이 복제과정에서 데이타를 읽을때, 이 3 노드 중에서 데이타를 몇개의 노드에서 데이타를 읽어올지를 결정하는 것이 R-Value이며. 동시에 몇개의 Node에 Write할것인가를 결정하는 것이 W Value이다.
만약에 W-Value가 2이면 Write시에 동시에 두개의 Node에 Write한다. R-Value가 1보다 클 경우 R-Value 노드 에서 데이타를 읽어오고, 두 개의 데이타가 다를 경우 최근 데이타를 사용한다.

이런 이유로 R-Value + W-Value > N-Value이면 Data Consistency가 보장된다.
예를 들어 N=3일때, R=2,W=2이면, Write시 적어도 두개의 복제본에 썼기 때문에, 하나가 복제가 안되어 있다하더라도, R-Value가 2이기 때문에, 꼭 하나는 새로운 데이타가 읽어지게 되고, 새로운 데이타를 Winning하는 정책 때문에 항상 최신의 데이타를 읽을 수 있다. (여기서 새로운 데이타를 판단 가능하게 하는 방법을 Vector-Clock이라고 한다. 이는 나중에 따로 포스팅 예정)

참고 : http://wiki.apache.org/cassandra/ArchitectureOverview 

Consistency

See also the API documentation.

Consistency describes how and whether a system is left in a consistent state after an operation. In distributed data systems like Cassandra, this usually means that once a writer has written, all readers will see that write.

On the contrary to the strong consistency used in most relational databases (ACID for Atomicity Consistency Isolation Durability) Cassandra is at the other end of the spectrum (BASE for Basically Available Soft-state Eventual consistency). Cassandra weak consistency comes in the form of eventual consistency which means the database eventually reaches a consistent state. As the data is replicated, the latest version of something is sitting on some node in the cluster, but older versions are still out there on other nodes, but eventually all nodes will see the latest version.

More specifically: R=read replica count W=write replica count N=replication factor Q=QUORUM (Q = N / 2 + 1)

  • If W + R > N, you will have consistency

  • W=1, R=N
  • W=N, R=1
  • W=Q, R=Q where Q = N / 2 + 1

Cassandra provides consistency when R + W > N (read replica count + write replica count > replication factor).

You get consistency if R + W > N, where R is the number of records to read, W is the number of records to write, and N is the replication factor. A ConsistencyLevel of ONE means R or W is 1. A ConsistencyLevel of QUORUM means R or W is ceiling((N+1)/2). A ConsistencyLevel of ALL means R or W is N. So if you want to write with a ConsistencyLevel of ONE and then get the same data when you read, you need to read with ConsistencyLevel ALL.



4. Masterless Architecture
또다른 특징 중에 하나는 Masterless 아키텍쳐이다. Ring을 구성하고 있는 아무 Node에나 요청을 보내도 처리가 되고, 전체 설정 정보를 가지고 있는 마스터 노드나 Admin 노드가 없다.
10개의 노드를 가지고 있는 Ring의 아무 Node에나 Request를 하더라도, 각 Node는 해당 데이타가 다른 어느 Node에 저장되어야 하는지를 Consistent Hash를 통해서 알 수 가 있고, 해당 노드로 Request를 Routing한다. 이때 자기가 데이타를 가지고 있지 않더라도 첫번째 요청을 받은 Node가 데이타를 처리하는 이 노드를 Coordinator Node라고 정의한다.

지금까지 대략적인 Dynamo 아키텍쳐의 특성에 대해서 알아보았다. 그러면 어떤 장단점이 있을까?

장점
1. High Availibility & Partition Tolerence
위와 같은 특성 때문에, 분산 시스템의 CAP 이론에서 A와 P에 최적화 되어 있다. 특정 노드가 장애가 나더라도 서비스가 가능하며 (A-Availibility) Node간의 네트워크 통신이 끊어지더라도 서비스가 가능하다 (P-Partition Tolerance : Vector Clock을 이용하여 데이타의 정합성을 처리가 가능하고 각 노드가 독립적으로 서비스가 가능

2. No Sigle Failure Point. No Master Node
그리고 Masterless 아키텍쳐로 인해서 Single Failure Point (SFP)가 없다. 이는 대규모 분산 환경에서 아주 큰 장점 중의 하나인데, 무제한 확정된 클러스터라도, 특정 노드 장애에 대해 종속이 되어 버리면 시스템의 안정성에 많은 영향을 미친다.

단점
반대로 단점은
1. Cannot Change Ring Size
일반적인 Dynamo 기반의 아키텍쳐는 Ring Size (Partition 수)를 변경할 수 없다. 데이타가 이미 Partition 별로 분산 저장되어 있기 때문에 파티션의 개수를 변경하면 다시 데이타를 변경된 파티션 수 에 따라 재 분배해야 한다. 이는 데이타의 이동을 초래하고, 많은 IO 부하를 유발하기 때문에 운영환경에서는 거의 불가능하다고 봐야 한다.

2. Data InConsistency
앞에서 설명한 바와 같이 데이타 복제가 실시간이 아니기 때문에 데이타에 대한 불일치가 발생한다. 물론 R,W Value를 조정해서 Consistency를 보장받는 방안은 있지만, 이 값을 높일 경우 동시에 여러 노드에 Read 또는 Write를 해야 하기 때문에 성능저하가 발생할 수 있고, 또한 Node간에 네트워크가 단절되는 Partitioning이 발생했을 때도 서비스는 되기 때문에, 다시 장애가 극복되었을때는 당연히 Data InConsistency가 발생하게 된다.

3. Sibling (Data Conflict 발생)
특히 네트워크 Partitioning이 발생하거나 또는 동시에 두개의 Client가 Write를 했을때, Vector-Clock 값이 똑같아서 어느 데이타가 더 최근 데이타인지 판단할 수 없는 Data Conflict (Sibling현상)이 발생한다. (Sibling의 자세한 개념은 나중에 Vector Clock 설명에 같이 추가)
저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

페이스북의 Photo 서비스 시스템 아키텍쳐
The Photos application is one of Facebook’s most popular features. Up to date, users have uploaded over 15 billion photos which makes Facebook the biggest photo sharing website. For each uploaded photo, Facebook generates and stores four images of different sizes, which translates to a total of 60 billion images and 1.5PB of storage. The current growth rate is 220 million new photos per week, which translates to 25TB of additional storage consumed weekly. At the peak there are 550,000 images served per second. These numbers pose a significant challenge for the Facebook photo storage infrastructure.

1.5 PB의 사진 저장. 2009년 아키텍쳐 문서

http://www.facebook.com/note.php?note_id=76191543919

얻은 아이디어
- 하드웨어 디자인시, 디스크에 Write Back 캐쉬 적용

하나의 이미지 파일이 올라오면 4개의 Resize된 thumnail을 유지.
하나의 PhotoKey에 대해서, 1,2,3,4 thumnail에 대한 reference를 저장
Photo Key + Thumnail reference 를 Index형태로 저장하고 메모리에 유지하여 성능 향상

물리 저장 파일과 (HeyStack Store File)
Index 파일을 나눠서 저장하는 데이타 구조 (HeyStack Index File)-Index 파일은 물리 저장 파일을 통해서 다시 Build가 가능
물리 파일을 먼저 기록한 다음, Index를 Async 방식으로 기록하는 방식. Index 파일은 위와 같은 이유로 Less Critical
Delete Operation은 Index Flag에 deleted로 표시 해놓고, 물리 저장 파일은 Compaction 단계에서 collection
Index는 모두 Memory에 Loading하는 방식을 사용

Summary
Write를 Append 방식으로 하고, search나 read 성능 향상을 위해서 index 파일을 별도 유지하고, index는 memory에 로딩되는 방식을 사용. Update나 Delete는 New file을 append하고, Compaction 단계에서 Copy&Scavange와 유사한 방식으로, deleted로 mark된 needle(file)을 제거하고 복사하는 형태를 사용함



 

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
 

티스토리 툴바