kubernetes volume_1

|

쿠버네티스 volume_1

볼륨이란?

컨테이너의 파일시스템

  • 볼륨을 이해하기전에 파드 내부 컨테이너의 파일시스템 특성을 이해해야한다.
  • 컨테이너 마다 고유한 파일시스템을 가지는데 파일시스템은 컨테이너 이미지에서 제공되기 때문이다.
  • 컨테이너가 재시작되면 이전에 쓰여진 파일들은 볼 수 없다. (가령 로그파일같은 것들)

쿠버네티스의 볼륨

  • 파드 내부 컨테이너들이 재시작되더라도 유지되는 데이터들을 디렉토리에 쓰고자할 때 볼륨을 사용한다.
  • 볼륨은 독립적인 쿠버네티스 오브젝트가 아니기때문에 자체적으로 생성, 삭제되지 않으며 파드의 스펙에서 정의된다. 따라서 파드와 동일한 라이프사이클을 가진다. 즉 파드가 삭제되면 볼륨 또한 삭제되는 것이다.
  • 하지만 볼륨을 정의하면 컨테이너의 라이프사이클과는 무관하게 이전에 기록된 모든 파일들을 컨테이너가 볼 수 있다.
  • 볼륨은 파드 내부의 모든 컨테이너에서 사용 가능하나 접근하기 위해선 반드시 컨테이너 각각에서 마운트 되어야한다.

볼륨의 유형

  • 쿠버네티스는 다양한 유형의 볼륨을 지원한다.
  • emtyDir: 일시적인 데이터를 저장할 때 사용되는 비어있는 디렉토리
  • hostPath: 워커 노드의 파일시스템을 파드의 디렉토리로 마운트하는데 사용
  • gitRepo: 깃 저장소의 컨텐츠를 체크아웃해 초기화한 볼륨
  • nfs: NFS 공유를 파드에 마운트
  • 그밖에 볼륨들은 gcePersistentDisk, awsElasticBlockStore, azureDisk, cinder, cephfs, iscsi ,,,
  • 특별한 유형의 볼륨: configMap, secret, downwardAPI 이들은 데이터를 저장하는 데 쓰이지 않고 쿠버네티스 메타데이터를 파드에 실행 중인 애플리케이션의 노출하는데 사용된다. (secret의 예로 private docker registry에 접근하기 위한 레지스트리 호스트, 인증정보등을 저장해둠)

emptyDir

파드와 같은 라이프사이클을 가지는 볼륨 유형 중 하나인 emptyDir은 볼륨이 빈 디렉토리로 시작된다. emptyDir은 동일 파드에서 실행 중인 컨테이너 간 파일을 공유할 때 유용하다.

emptyDir 볼륨 사용의 예

k8s emptyDir을 사용하는 볼륨 예제 링크

  • 하나의 볼륨(html)을 동일한 파드(emtpy-dir-practice) 내부 각각 컨테이너들이(checkout, web-server) 마운트하여 데이터를 공유하는 예제이다.

hostPath

대부분의 파드는 호스트 노드를 인식하지 못하므로 노드의 파일시스템에 있는 어떤 파일에도 접근하면 안된다. 하지만 특정 시스템 레벨의 파드(가령 노드의 로그를 저장하는 /var/log 디렉토리나 컨테이너 로그를 저장하는 /var/lib/docker/containers 디렉토리, 이를 fluentd가 해당 디렉토리를 마운트하여 로그를 수집함)는 노드의 파일을 읽거나 파일 시스템을 통해 노드 디바이스를 접근하기 위해 노드의 파일시스템을 사용해야한다. 이를 hostPath 볼륨을 통해 가능하게 한다. hostPath 볼륨은 노드 파일시스템의 특정 파일이나 디렉토리를 가리킨다. 동일한 노드에서 실행중인 두 개의 파드가 있다고 가정하면 hostPath 볼륨의 동일한 경로를 사용 중이면 동일한 파일을 가리킨다. 데이터가 저장되는 위치가 노드이므로 파드가 삭제되더라고 데이터는 노드에 남아있으므로 이는 퍼시스턴트 스토리지이다. 단, 동일한 노드에 스케줄링된다는 전제하에 새로운 파드는 이전 파드가 남긴 데이터를 접근할 수 있다. 이런 전제조건으로 hostPath 볼륨을 데이터베이스의 데이터 디렉토리를 저장할 위치로 사용하기엔 문제가 있다. 보통 쿠버네티스에서 실행 중인 파드를 살펴보면(주로 namespace가 kube-system) hostPath 볼륨을 자체 데이터를 저장하기 위한 목적으로 사용하기 보다 노드 데이터에 접근하기 위해 사용한다.

python generator

|

iterator와 generator

generator를 이해하기전에 먼저 iterator에 대해 간단히 알아보자. iteratorlist, set, dictionary, str, bytes, tuple, range와 같은 iterable한 타입이나 collections을 차례로 꺼낼 수 있는 객체이다. generator 또한 iterable한데, 해야 할 일을 마치면 반환하지 않고 대기 상태가 된다. 그 후 다시 호출되면 일을 이어서 진행하는 특징을 가지고 있다. 또한 generatorlazy iterator로서 메모리 리소스를 적게 쓸 수 있다. 이러한 특징으로 거대한 양의 iterable한 객체들을 다룰 때, 무한 sequence를 다룰 때 사용된다.


예제) large files 다룰 때

csv_list = csv_reader("huge_csv.txt")
row_content = 0

for row in csv_list:
    row_count += 1

print(f"Row count is {row_count}")

def csv_reader(file_name):
    file = open(file_name)
    result = file.read().split("\n")
    return result

아주 큰 csv파일을 열어 \n으로 쪼개어 result에 담게되면 MemoryError 혹은 시스템이 느려지는 상황이 발생할 것이다. 이러한 상황을 회피할 수 있는 방법중 하나가 generator를 사용하는 것이다.

def csv_reader(file_name):
    for row in open(file_name, "r"):
        yield row
Row count is 912349023 # Awesome!

위 예제를 보면 csv_reader에 yield가 추가 되었다. 함수에 yield가 있으면 generator가 만들어지게 된다 (yield는 나중에 좀 더 자세히 살펴보기로하고) yield를 만난 iterator는 실행을 잠시 중단하고 caller에게 값(generator object)을 전달하는 것이다. 이는 iteratorStopIteration을 만나기전까지 반복하여 실행된다.


generator type

generator는 두 가지 타입이 있는데 하나는 위 예제에서 볼 수 있는 yield를 가지는 functionsgenerator expressions이다.

def csv_reader(file_name):
    file = open(file_name)
    return (row for row in file.read().split("\n")) # generator expressions

예제) 무한한 시퀀스를 생성할 때

def infinite_seq():
    num = 0
    while True: # infinite ...
        yield num # gernerator!
        num += 1
>>> for i in infinite_seq():
...     print(i, end=" ")
...
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39 40 41 42
[...]
6157818 6157819 6157820 6157821 6157822 6157823 6157824 6157825 6157826 6157827
6157828 6157829 6157830 6157831 6157832 
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

위 예제를 통해 generator는 KeyboardInterrupt가 발생하지 않는다면 무한히 값을 찍어낸다. 함수가 실행될 때 generator는 loop를 이용하지 않고도 next()함수를 이용하여 순차적으로 실행할 수 있는데 iterator object안에는 __next__()가 있기 때문이다. next()에 의해 실행되는 generator는 할 일을 마치면 대기상태에 들어갔다가 일을 이어한다는 것을 확인할 수 있다.

>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1

generator의 성능

앞서 살펴보았듯 generator는 메모리를 최적으로 사용하기에 상당히 좋은 방법이였다. 실제 list와 generator의 메모리 사이즈를 sys.getsizeof()를 이용하여 살펴보겠다.

>>> import sys
>>> nums_squared_lc = [i*2 for i in range(100000)]
>>> sys.getsizeof(nums_squared_lc)
824464 
>>> nums_suared_gc = (i*2 for i in range(100000))
120

list의 경우 824,464 bytes이고 generator의 경우 120 bytes이다. listgenerator에 비해 약 700배의 메모리 공간을 가지게된다. 하지만, 공간이 아닌 속도를 살펴보면 결과가 어떨까?

>>> import cProfile
>>> cProfile.run('sum([i*2 for i in range(100000)])')
    100005 function calls in 0.025 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   100001    0.014    0.000    0.014    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    0.025    0.025 <string>:1(<module>)
        1    0.000    0.000    0.025    0.025 {built-in method builtins.exec}
        1    0.011    0.011    0.025    0.025 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
>>> cProfile.run('sum((i*2 for i in range(100000)))')
    5 function calls in 0.010 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    10001    0.002    0.000    0.002    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    0.003    0.003 <string>:1(<module>)
        1    0.000    0.000    0.003    0.003 {built-in method builtins.exec}
        1    0.001    0.001    0.003    0.003 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

위 결과값을 보면 list의 경우 generator보다 약 2.5배 빠른것을 살펴볼 수 있다. 따라서 풀어야하 할 문제는 공간과 속도를 고려해 적절한 타입을 선택해 해결해야한다.


yield

yield를 가지는 functions은 generator이다. yield를 통해 callee는 pause되고 caller에게 값을 던져준다. 이는 StopIteration을 만날때까지 이어진다.

>>> def multi_yield():
...     y_str = "Fisrt yield"
...     yield y_str
...     y_str = "Second yield"
...     yield y_str

>>> import dis
>>> dis.dis(multi_yield)
  2           0 LOAD_CONST               1 ('First yield')
              2 STORE_FAST               0 (yield_str)

  3           4 LOAD_FAST                0 (yield_str)
              6 YIELD_VALUE
              8 POP_TOP

  4          10 LOAD_CONST               2 ('Second yield')
             12 STORE_FAST               0 (yield_str)

  5          14 LOAD_FAST                0 (yield_str)
             16 YIELD_VALUE
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

위 예제의 8라인을 보면 yield를 통해 POP_TOP으로 top-of-stack을 지워버릴 뿐 함수를 반환하지 않는것을 살펴볼 수 있다. 이 때 caller에게 값을 넘겨주고 pause상태에 들어간다. 지금까지는 yield를 통해 caller에게 callee가 값을 넘겨주기만 한다. 즉 일방향이다. 양방향으로 값을 주고 받을 순 없을까? 해답은 바로 send()이다.


send()를 통한 coroutine

generatoryield를 통해서 값을 caller에게 넘겨주는데 반대로 caller가 값을 넘겨주고 싶을 때 바로 send()를 사용한다.

def score_generator():
    score = 0
    default = 0.5
    while True:
        incr = yield score # caller에게 score를 넘긴다.
        score += incr if incr is not None else default
>>> s_gen = score_generator()
>>> next(s_gen)
0
>>> next(s_gen)
0.5
>>> next(s_gen)
1
>>> s_gen.send(5)
6
>>> s_gen.send(1.5)
7.5
>>> next(s_gen)
8

send()함수를 이용해 yield를 실행하는 지점에 값을 callee에게 줄 수 있다. 이 기법이 바로 generator기반의 coroutine이며 python3에서는 이를 native coroutine으로 발전시켰다.


throw()와 close()

특정한 상황에서 generator를 종료(default: StopIteration)하고 싶다면 throw()close()를 사용하면 된다.

>>> s_gen = score_generator()
>>> for s_g in s_gen:
...    accum_score = s_gen.send(1)
...    print(accum_score)
...    if accum_score > 1:
...        s_gen.throw(ValueError("Accum_score large then 5"))
...        # s_gen.close()


ValueError                 Traceback (most recent call last)
<ipython-input-80-2bb73b6a4b0a> in <module>
      3     accum_score = s_gen.send(1)
      4     if accum_score > 1:
----> 5         s_gen.throw(ValueError("Accum_score large then 5"))
      6         # s_gen.close()
      7 

<ipython-input-72-0198c1120a1c> in score_generator()
      3     default = 0.5
      4     while True:
----> 5         incr = yield score # caller에게 score를 넘긴다.
      6         score += incr if incr is not None else default
      7 

ValueError: Accum_score large then 5

결론

  • python의 generator의 특징과 선언법을 알아봤다.
  • 해결할 문제에 공간과 속도를 고려해 generator를 선택할 수 있다.
  • generator 기반의 coroutine을 살펴보았기 때문에 비동기식 프로그래밍을 이해할 수 있는 준비가 되었다.

reference

python decorator

|

Decorator ?

데코레이터란 특별한 함수나 클래스로 동작한다. 이 기능이 가능한 이유는 파이썬은 First-Class function이기 때문이다. 아래는 First-Class function의 대표적인 특징이다.

  • 변수나 데이터 구조안에 담을 수 있다.
  • 함수의 파라미터로 전달할 수 있다.
  • 반환 값으로 사용할 수 있다.(closure)
  • 동적으로 프로퍼티 할당이 가능하다.

Inner function

본격적으로 데코레이터에 들어가기전에 closure의 개념을 살펴볼 수 있는 inner function을 살펴보자. closure란 First-Class function을 지원하는 언어의 네임 바인딩 기술이다. 함수안에 선언된 내부 함수를 반환하고 이를 함수 외부에서 접근이 가능하게 된다.

# closure example

def parent_func(child):
    print("Parent")
    def first_child():
        return "First child"
    def second_child():
        return "Second child"
    if child == 1:
        return first_child
    else:
        return second_child 

위 예제에서 부모함수 내부에 선언된 두 개의 자식함수를 조건문에 의해 반환한다. 아래의 예제를 통해 어떤 일이 일어나는지 살펴보자.

>>> first = parent_func(1)
>>> 'Parent'
>>> first
<function __main__.first_child>
>>> first()
'First child'

first 변수에 부모함수에서 반환한 first_child 함수를 참조하고 first를 실행하면 first_child 함수를 실행하게된다. 이러한 특징을 이용해 데코레이터를 구현할 수 있다.


Simple decorator

def simple_decorator(func):
    def wrapper():
        print('Before the function is called.')
        func()
        print('After the function is called.')
    return wrapper

def say_hello():
    print('Hello!')

@simple_decorator
def say_hi():
    print('Hi!')
>>> hello = simple_decorator(say_hello)
>>> hello
<function __main__.wrapper>
>>> hello()
Before the function is called.
Hello!
After the function is called.
>>> say_hi
<function __main__.wrapper>
>>> say_hi()
Before the function is called.
Hi!
After the function is called.

say_hello 예제는 First-Class function 특징을 이용해 함수의 파라미터에 함수를 보내고, 내부 함수 반환을 통해 외부(say_hello)에서 내부 함수(wrapper)를 접근하는 것을 구현한 예제이다.
say_hi 예제는 데코레이터를 사용했는데 외부 함수(say_hi)위에 데코레이터를 정의하여 구현한 예제이다.


Decorating functions with arguments

def arg_decorator(func):
    def wrapper(*args, **kwargs):
        print('wrapper: Hi! {}'.format(args[0]))
        func(*args, **kwargs)
    return wrapper

@arg_decorator
def say_hi(name):
    print('Hi! {}'.format(name))

say_hi에 name변수를 실으면 데코레이터 선언에 의해 arg_decorator 내부 함수인 wrapper의 파리미터로 전달이 된다. 전달된 파라미터는 wrapper 함수내부에서 사용가능하며 이를 이용한 동작이 외부 함수인 say_hi를 통해 일어난다.

>>> say_hi
<function __main__.wrapper>
>>> say_hi('Cole')
wrapper: Hi! Cole
Hi! Cole

Returning values from decorated functions

def return_decorator(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs) # 내부 함수의 반환으로 외부 함수의 반환값이 동작 
    return wrapper

@return_decorator
def say_hi(name):
    print('Hi! {}'.format(name))
    return 'I\'m tyler'

>>> say_hi('Cole')
Hi! Cole
Hi! Cole
I'm tyler

위 코드를 자세하게 이해하기 위해 inspect 모듈을 이용해 frame구조를 살펴보자.

import inspect

frame_return_decorator = None
frame_wrapper = None
frame_say_hi = None
def return_decorator(func):
    global frame_return_decorator
    frame_return_decorator = inspect.currentframe()
    def wrapper(*args, **kwargs):
        global frame_wrapper
        frame_wrapper = inspect.currentframe()
        return func(*args, **kwargs)
    return wrapper
    
@return_decorator
def say_hi(name):
    for x in inspect.stack():
        print(x)
    global frame_say_hi
    frame_say_hi = inspect.currentframe()
    print('Hi! {}'.format(name))
    return 'I\'m tyler'
>>> say_hi('Cole')
(<frame object at 0x7f915b174fb0>, '<ipython-input-86-ee5e8e3c7b70>', 17, 'say_hi', [u'    for x in inspect.stack():\n'], 0)
(<frame object at 0x1022b9608>, '<ipython-input-86-ee5e8e3c7b70>', 12, 'wrapper', [u'        return func(*args, **kwargs)\n'], 0)
(<frame object at 0x1022b9da8>, '<ipython-input-87-3ebe088a51a6>', 1, '<module>', [u"say_hi('Cole')\n"], 0)
...
Hi! Cole
I'm tyler
>>> frame_return_decorator.f_back
<frame at 0x7f915b3756f0>
>>> frame_wrapper.f_back
<frame at 0x1022b9da8>
>>> frame_say_hi.f_back
<frame at 0x1022b9608>

먼저 stack()함수에 의한 frame의 stack을 살펴보면 return_decorator(wrapper(say_hi))순서의 호출을 확인할 수 있다. say_hi 함수가 stack의 최상위에 있다는 것을 알 수 있다. inspect.currentframe()의 attributes 중 f_back은 caller의 frame을 나타낸다. say_hi의 f_back은 wrapper의 frame과 같은 것을 확인할 수 있다. 즉 say_hi의 반환은 wrapper의 반환이 없다면 값이 최종 프레임에 전달되지 않는다.


Functools decorator

데코레이터를 인터프리터에서 확인해보면 내부함수인 wrapper를 참조하는 것을 확인할 수 있는데, 이는 디버깅시 문제가된다. 따라서 functools 모듈을 이용해서 실제 데코레이터의 함수 정보를 얻어야한다.

import functools

def simple_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func(*args)
    return wrapper

@simple_decorator
def say_hi(name):
    print('Hi! {}'.format(name))
>>> say_hi
<function __main__.say_hi>
>>> help(say_hi)
Help on function say_hi in module __main__:

say_hi(*args, **kwargs)

정리

python은 First-class function을 지원하는 언어이다. 이러한 특징으로 closure를 이용해 데코레이터를 구현하다.
이는 특정 함수의 시작 전, 시작 후에 동작하는 로직을 구현할 때 유용하다. 유효성 검사, 로그 관리 등 함수가 가지는 실제 코어 로직과는 별도로 진행되는 역할을 데코레이터를 통해 구현함으로써 코드의 재사용을 줄일 수 있다.

reference

realpython: Primer on Python Decorators