본문 바로가기

엔지니어/Linux

Python과 Django 기반의 모니터링 시스템, Hubblemon

728x90
반응형

Hubblemon은 Python과 Django 기반의 모니터링 시스템입니다. 네이버의 memcached 기반 메모리 캐시 클라우드인 Arcus를 모니터링하는 목적으로 개발을 시작했으나, 오픈소스로 공개하면서 memcached, Redis,MySQLCUBRIDjstat 등을 위한 플러그인도 추가했습니다.

이 글에서는 Python 구문으로 쉽게 그래프와 대시보드를 구성할 수 있는 Hubblemon의 기본 설계 방식을 살펴보고, 이러한 설계 방식으로 확장될 수 있는 eval 구문의 활용과 데이터 분석 방법, 시스템 설정 방법을 설명하겠습니다. 그리고 마지막으로 Hubblemon 인스턴스의 구성을 간략하게 설명하겠습니다. 플랫폼 개발자나 Python 사용자라면 재미있게 볼 수 있을 것이라 생각합니다.

유연한 그래프 구성

사용자 입장에서 모니터링 시스템을 접할 때 가장 아쉬웠던 점은 수집한 데이터를 활용해 원하는 항목으로 이루어진 그래프와 대시보드를 구성하고 필요에 따라 변경하기가 쉽지 않았다는 점이다. 이런 아쉬운 점을 해소하려 Hubblemon에서는 Python 구문으로 그래프와 대시보드를 쉽게 구성할 수 있게 했다. 예를 들어 다음 그림은 Hubblemon의 메뉴에서 arcus_stat를 선택하면 나타나는 Arcus 캐시 클라우드 모니터링 화면이다.

그림 1 Hubblemon의 대시보드

그림 1 Hubblemon의 대시보드

Arcus 캐시 노드에서 수집한 통계 정보 가운데 서로 관련이 있는 정보를 같은 그래프에 모아서 보여 주거나, 값을 합치거나 비율을 계산한 결과를 그래프로 보여 준다. 인스턴스가 클라우드나 그룹을 구성할 경우 인스턴스별 그래프를 보여 주거나 인스턴스를 합쳐서 같은 그래프에 보여줄 수도 있다.

물론 대부분의 모니터링 도구가 사용자 설정으로 그래프의 항목과 대시보드의 구성을 바꿀 수 있지만 보통 사전에 정해진 틀 안에서만 할 수 있다. 반면 Hubblemon은 그래프의 항목과 대시보드의 구성을 Python 구문으로 설정할 수 있으며, Python의 eval 구문을 이용해 서비스 시점에 사용자가 다시 정의할 수 있다.

그림 1의 Arcus 모니터링 화면은 다음과 같이 리스트 형식으로 된 arcus_preset이라는 가공 필터와arcus_view() 함수로 구현된다.

arcus_preset = [['bytes', 'total_malloced', 'engine_maxbytes'], (lambda x: x['get_hits'] / x['cmd_get'] * 100, 'hit_ratio'), ['hb_count', 'hb_latency'], ['rusage_user', 'rusage_system'], 'curr_items', 'evictions', ... ]

def arcus_view(path, title = ''):  
    return common.core.loader(path, arcus_preset, title)

사용할 데이터의 위치(설정에 따라서 Hubblemon이 설치된 노드일 수도 있고 원격에 있는 노드일 수도 있다)를path 파라미터로 전달한다. 전달 받은 위치에 있는 데이터를 arcus_preset 가공 필터를 거쳐 최종 그래프 목록으로 생성하는 역할은 common.core.loader 객체(이하 loader 객체)가 하며, arcus_view()는 이 과정을 함수로 만든 것이다. 결국 그림 1의 화면을 구성하는 데는 가공 필터로 쓰인 arcus_preset 하나만으로 충분하다.

모니터링 화면의 그래프는 가공 필터의 리스트에 있는 요소가 하나씩 나타난 것이다. 가공 필터에서 ['bytes', 'total_malloced', 'engine_maxbytes']와 같이 리스트로 구성된 요소를 만나면 첫 번째 그래프처럼 같은 그래프에 데이터를 그린다.

그림 2 ['bytes', 'total<em>malloced', 'engine</em>maxbytes'] 리스트로 그린 그래프

그림 2 ['bytes', 'totalmalloced', 'enginemaxbytes'] 리스트로 그린 그래프

주목할 것은 두 번째 요소다. 그래프의 값을 람다 함수로 얻었다.

arcus_preset = [..., (lambda x: x['get_hits'] / x['cmd_get'] * 100, 'hit_ratio'), ...]  

가공 필터에서 튜플로 이루어진 요소를 만나면 loader 객체는 튜플의 요소가 실행 가능한 함수인 경우 그래프를 구성하는 요소를 딕셔너리로 전달해 실행한다. 튜플의 요소가 문자열이면 그래프의 제목으로 설정한다. 위의 람다 함수는 cmd_get 값과 get_hits 값의 비율에 100을 곱해 캐시 적중률(hit_ratio)을 구하고 있다. 그림 1에서 두 번째 그래프에 해당한다.

그림 3 람다 함수로 그린 캐시 적중률 그래프

그림 3 람다 함수로 그린 캐시 적중률 그래프

람다 함수를 사용하면 데이터를 합치거나 여러 형태로 가공할 수 있다. 이처럼 Python 구문을 사용하면 간결한 언어로 화면을 쉽게 구성할 수 있을 뿐만 아니라 eval 구문으로 기능을 확장할 수 있다.

eval 구문을 사용한 확장

"유연한 그래프 구성"에서 설명했듯이 loader 객체에서 사용한 가공 필터로 그래프와 대시보드의 구성을 설정할 수 있다. 그러나 이 설정은 개별 플랫폼 플러그인을 개발하는 단계에서 이미 이루어진 고정된 설정이다.

사용성이 좋은 모니터링 도구는 사용자가 브라우저에서 그래프와 대시보드의 구성을 수정할 수 있는 UI를 제공한다. 하지만 이런 도구에서도 미리 정해진 틀 안에서만 수정할 수 있다. 그리고 UI 구현에 손도 많이 간다. Hubblemon은 개발 인력의 한계로 구현에 손이 적게 가면서도 확장성이 있는 방식으로 그래프와 대시보드를 수정할 수 있게 접근했다.

Hubblemon에서 데이터를 읽고 그래프를 구성하는 객체인 loader 객체를 반환하는 구문을 사용자가 입력하면 Python의 eval() 함수를 사용해 구문을 실행하고 결과를 보여 주는 방식이다. 이런 방식으로 Hubblemon의 메뉴에서 expr를 선택하면 나오는 expr 페이지에서 원하는 그래프와 대시보드를 구성할 수 있다.

예를 들어 expr 페이지의 expr에 다음과 같이 입력한다.

loader('test.arcus/arcus_11211', [(lambda x: x['get_hits'] / x['cmd_get'] * 100, 'hit_ratio'), 'cmd_get', 'get_hits'])  

eval을 클릭해 입력한 구문을 실행하면 다음과 같은 결과 화면이 나타난다.

그림 4 expr 페이지에서 eval 구문을 실행해 그린 그래프

그림 4 expr 페이지에서 eval 구문을 실행해 그린 그래프

"유연한 그래프 구성"에서 설명한 대로 loader 객체는 'test.arcus/arcus_11211'에서 데이터를 받는다. 'test.arcus/arcus_11211'은 test.arcus 클라이언트의 정보가 위치한 곳에 있는 11211 포트의 Arcus 통계 정보를 의미한다. 그래프를 그릴 항목을 선택하고 가공하는 필터는 두 번째 파라미터에 설정한다.

다음은 좀 더 복잡한 예로, merge([loader(…),loader(…)]) 구문을 사용해 두 시스템의 CPU 사용률을 합쳐서 그린 그래프다. merge() 함수는 리스트에 요소로 있는 loader 객체의 반환 결과를 합쳐서 그래프를 그리는 함수다.

그림 5 merge() 함수를 활용해 그린 그래프

그림 5 merge() 함수를 활용해 그린 그래프

유사하게 sum() 함수, filter() 함수 등으로 통계 자료를 합치거나 필터링할 수 있다. 다음은 sum() 함수를 사용해 memcached의 get 명령의 횟수(cmd_get)와 이에 대한 이동평균을 계산해 그린 그래프다. 주황색이 cmd_get이며 하늘색이 과거 10회의 이동평균선, 빨간색이 과거 100회의 이동평균선이다.

그림 6 cmd_get 정보의 이동평균선 그래프

그림 6 cmd_get 정보의 이동평균선 그래프

사용한 구문은 다음과 같다. cmd_get의 값과 이동평균을 구하는 람다 함수로 필터를 구성했다.

loader('con01.arcus/arcus_11211', [['cmd_get', (lambda x: sum(x['prev']['cmd_get'][-10:])/10, 'average 10'), (lambda x: sum(x['prev']['cmd_get'][-100:])/100, 'average 100')]])  

전달되는 인자의 누적 데이터가 저장된 x['prev']로 접근한 다음 cmd_get의 데이터 가운데 마지막 10개와 100개를 sum() 함수로 모두 더하고, 그 총합을 10과 100으로 나눈 결괏값을 사용해 그래프를 그린다.

이와 같은 유용한 표현식 몇 가지를 익혀 두면 어떤 형태로든 필요한 대로 데이터를 가공해 조회할 수 있다.

데이터 분석

eval 구문은 Python 구문을 무엇이든 실행할 수 있기 때문에 단순히 그래프를 그리는 것 외의 다른 작업도 실행할 수 있다.

무엇을 할 수 있을까? Hubblemon은 모니터링하고 있는 모든 인스턴스의 목록과 그룹 정보를 알고 있고 각각의 모니터링 데이터에 접근할 수 있다. 다시 말해 개별 데이터로 그래프를 생성하는 작업 말고 전체 데이터를 조사, 가공 작업도 수행할 수 있다.

이를 쉽게 하기 위해 Hubblemon의 내부 함수로 for_each(client_list, filter, output_generator) 함수를 정의했다. for_each() 함수는 client_list 파라미터로 받은 클라이언트의 데이터를 loader 객체로 열어filter 파라미터에 전달, 실행하고 결과가 참인 객체에 대해 output_generator 파라미터로 그래프를 그린다.

다음 그림은 Hubblemon에서 모니터링하고 있는 모든 서버에서 네트워크 사용량이 초당 60MB를 넘는 넘는 서버를 찾는 모습이다.

그림 7 for_each() 함수를 활용해 그린 그래프

그림 7 for_each() 함수를 활용해 그린 그래프

위의 그래프에 사용한 구문은 다음과 같다.

for_each(get_all_data_list('psutil_net'),lambda x: x.max('bytes_sent') + x.max('bytes_recv') > 1000000 * 60, lambda x: loader(x, [['bytes_sent', 'bytes_recv']], title=x))  

get_all_data_list(prefix) 함수는 모든 서버의 모니터링 항목 가운데 prefix 파라미터의 값으로 시작되는 항목을 리스트로 반환한다. 예제에서는 psutil_net을 prefix 파라미터의 값으로 사용했는데, Hubblemon의 시스템 통계 정보를 psutil 모듈로 수집하기 때문이다.

시스템의 모니터링 항목을 loader 객체로 열어 두 번째 요소인 람다 함수로 필터링한다. loader 객체의 데이터 취합 함수인 max()avg()sum()으로 최근 30분간(for_each() 함수의 파라미터로 시간 범위도 지정할 수 있으나 기본값은 최근 30분이다)의 데이터 가운데 bytes_sent와 bytes_recv의 최댓값을 합친 값이 60MB를 넘는 Arcus 인스턴스를 필터링해 세 번째 파라미터에 리스트로 전달했다.

세 번째 파라미터는 필터링된 리스트를 loader 객체로 열어 bytes_sent와 bytes_recv를 항목으로 하는 그래프를 그린다.

이런 방법으로 Hubblemon에서 모니터링하는 모든 클라이언트 가운데 네트워크 사용량이 높은 클라이언트를 찾아 조치할 수 있다. 유사하게 CPU나 디스크의 과부하, 저부하를 조사할 수도 있고, 사고 발생 시 해당 시점을 시간 범위로 설정해 특이 사항이 있었던 클라이언트를 찾을 때도 활용할 수 있다.

Hubblemon은 플러그인 클라이언트의 데이터를 서버 단위로 수집할 때 해당 서버 이름 아래에 플러그인 이름을 접두어(prefix)로 붙여서 저장한다. 따라서 for_each(get_all_data_list('redis_')...)는 모니터링하고 있는 전체 Redis 자료를 조사할 때 사용할 수 있고, for_each(get_all_data_list('cubrid_')…)는 CUBRID 데이터베이스를 조사할 때 사용할 수 있다.

코드를 조금 더 추가하면 더 유용하게 사용할 수 있다. Arcus의 경우 전체 클라우드의 목록을 반환하는arcus_cloud_list()란 함수와 클라우드 목록을 입력받아 그 클라우드를 구성하는 각각의 인스턴스 데이터를 반환하는 arcus_instance_list() 함수가 있어 전체 혹은 클라우드별로 for_each() 함수를 실행할 수 있다.

다음 그림은 전체 Arcus 서버에서 cmd_get의 값이 초당 100건 이하로 오는 저사용 인스턴스를 찾아 그래프로 그린 화면이다.

그림 8 저사용 Arcus 인스턴스의 cmd_get 그래프

그림 8 저사용 Arcus 인스턴스의 cmd_get 그래프

그림 7과 그림 8처럼 특정한 값을 넘거나 넘지 않는 클라이언트가 있으면 사용자에게 알람을 보내 즉시 조처해야 하는 때도 있다. Hubblemon은 통계 정보를 수집하는 시점에 사용자에게 알람을 보낼 수 있다. 하지만 수집한 시점의 통계 정보만 기준으로 알람을 보낸다. 대부분은 이런 기준으로 알람을 보내는 것으로 충분하다. 하지만for_each() 함수를 데몬으로 따로 실행해 정해진 시간 단위로 해당 구간 내의 전체 데이터를 대상으로 이상 현상을 분석하면 특정 시점의 정보만으로 이상 현상을 분석해야 하는 단점을 보완할 수 있다.

Python 구문을 사용한 설정과 유의할 점

Python 구문으로 그래프와 대시보드의 설정을 바꾼다는 설계는 설정 파일과 알람 등에도 계속 적용되었다.Hubblemon 설치 가이드에서 설명한 대로 Hubblemon의 설정 파일은 Python 스크립트로 작성된 settings.py 파일이다. 물론 이렇게 설정 파일을 스크립트로 작성하는 방식은 설정 파일에 문법 오류가 있을 때 서버의 오작동을 유발할 수 있어 일반적으로 기피하는 방식이다.

하지만 이런 방식으로 설정을 유연하게 할 수 있고, 알람 조건에도 람다식을 쓸 수 있다. 다음은 Hubblemon에서 수집한 통계 정보를 확인해 알람을 보내는 조건을 설정하는 항목이다.

alarm_conf_absolute = {  
                'default':{
                                'rusage_user':(500000, 1000000, None),
                                'rusage_system':(500000, 1000000, None),
                                'evictions':(80000, 100000, 120000),
                                'reclaimed':(80000, None, None),
                                'cmd_get':(40000, 80000, 200000),
                                'cmd_set':(40000, 80000, 200000),
                },
        }

alarm_conf_lambda = {  
                'linegame-*':{
                        lambda x, limit: (x['total_malloced'] / x['engine_maxbytes'] > limit, 
                        'ratio of total_malloced/engine_maxbytes(%f) exceeds %f' % (x['total_malloced'] / x['engine_maxbytes'], limit)) : (0.7, 0.7, 0.75),
                },
        }

alarm_conf_absolute 설정은 수집된 통계 정보의 절댓값만을 기준으로 알람 수준을 판단한다. 예를 들어cmd_get 정보는 '40,000', '80,000', '200,000'을 기준으로 'info', 'warning', 'emergency' 수준의 알람을 보낸다.

alarm_conf_lambda 설정은 메모리 사용률을 계산해 알람을 보내는 수준을 판단한다. 다음의 예에서는linegame-*으로 시작하는 Arcus 클라우드의 engine_maxbytes와 total_malloced의 비율, 즉 Arcus 서버의 메모리 사용률이 limit보다 큰지 검사한다. 참고로 튜플의 두 번째 요소는 알람 메세지를 동적으로 만드는 부분이다.

그 외에 Readme 파일의 "Query"에서 설명한 대로 플러그인이 지원하는 경우 각 클라이언트에 대한 질의(query)를 Python 스크립트로 실행할 수 있다. 일반적인 운영 도구가 단순 질의만을 지원하는데 반해 Hubblemon의 질의 실행은 다음과 같이 반복문 등 여러 구문을 사용해 실행할 수 있다. 다음 예에서 cursor는 Hubblemon에서 선택한 클라이언트 정보를 사용해 미리 설정한 객체다.

for i in range(0, 100):  
    cursor.execute('insert into t1 values(%d);' % i)

cursor.execute('select * from t1;')  
return_as_table(cursor, p)  

Python 스크립트로 설정 파일을 작성하고 구문을 실행하는 방식에는 문제점이 있다. 안정적인 운영에 문제가 될 수 있으며(예를 들어 설정 파일의 오류로 시스템이 구동되지 않음), 보안상 문제가 될 수 있고(예를 들어 eval 구문으로 시스템에 해를 끼칠 수 있음), 과부하를 초래할 수 있다(예를 들어 eval 구문에 복잡한 구문을 실행해 응답성을 저해).

그래서 이런 모니터링 시스템은 접근 대상 및 서비스 범위를 명확히 해야 한다. 원래 Hubblemon은 운영자와 서비스 개발자가 사내에서 모니터링용으로 사용하려고 만들었고, 보안성 측면에서는 eval() 함수와 exec() 함수를 사용하는 명령의 실행을 syslog로 기록하는 수준으로 구현했다.

Hubblemon을 오픈소스로 공개한 이후에는 보안성을 좀 더 보완할 예정이다. 접근 권한과 관련해서는 추후 계정별 접근 가능 서버와 클라우드에 대한 매핑을 구현할 예정이다. 이런 접근 권한 처리가 완료되기 전까지는 expr 페이지와 각 클라이언트의 질의 기능은 메인 메뉴에서 제외하고 운영하는 것이 좋다(메인 메뉴의 항목은common/settings.py 파일에서 설정할 수 있다).

Hubblemon 인스턴스 구성

마지막으로 Hubblemon의 프로그램 구성을 설명하겠다. 다음은 Hubblemon의 인스턴스 구성도다.

그림 9 Hubblemon 인스턴스 구성

그림 9 Hubblemon 인스턴스 구성

모니터링 대상인 클라이언트에는 collect client가 설치되어 collect server로 접속한다. collect server는 해당 클라이언트가 배정된 collect listener로 리다이렉트하도록 응답한다. collect client는 collect listener로 다시 접속해 통계 정보를 송신한다.

collect listener를 여러 개 두고 collect server가 재배정하게 한 이유는 운영 편의성과 확장성을 위해서다. 모니터링하려는 수많은 클라이언트에 필요한 설정을 모두 동일하게 유지할 수 있고, 필요 시 중앙에서 모니터링 대상 클라이언트의 통계 정보 저장 위치를 변경할 수 있다.

collect listener는 Hubblemon 웹이 설치된 로컬이나 다른 노드에 설치할 수 있다. 로컬에 설치할 경우에는 물리적 디스크 하나에 collect listener 하나만 실행할 것을 권장하며 I/O의 한계에 이르면 원격 위치에 collect listener를 설치해 확장할 수 있다.

Hubblemon 웹은 클라이언트의 통계 정보가 필요할 때 배정 위치를 참조해 로컬 디스크에서 직접 조회하거나 원격 위치에 있는 collect listener에게 정보를 요청한다.

앞에서 설명한 loader 객체의 경우 내부 데이터 핸들을 가지고 있는데 로컬일 경우 rrd_reader를, 원격일 경우remote_reader를 사용한다. remote_reader는 collect listener로 데이터를 요청하며 collect listener는 로컬의 데이터 핸들인 rrd_reader를 사용해 응답한다. 현재는 로컬 데이터 핸들이 RRD를 기본으로 사용하지만 데이터 핸들을 변경하면 다른 데이터 형식도 사용할 수 있다.

마치며

이상으로 Hubblemon 소개를 마친다. 이 글을 읽으면서 스크립트 구문을 이용하는 설계 방식을 거북해 하는 분도 있을 것이고 재미있게 본 분도 있을 것이다. 어느 쪽이든 프로그램의 디자인과 구성 전략에 대해 잠시나마 다시 한번 생각해 보는 시간을 가졌다면 좋겠다. 그리고 몇몇 분들 중 이것을 자극으로 또 다른 참신한 아이디어가 떠오르고 그것이 공유된다면 더 바랄 나위가 없겠다.


출처: http://d2.naver.com/helloworld/7307029

반응형