글쓴이 보관물: totoli78
Boot2Docker Volume Disk Resize
Boot2Docker에서 “장치에 남은 공간 없음” 오류가 발생합니까?
많은 수의 이미지와 함께 Boot2Docker를 사용하거나 작업 중인 이미지가 매우 큰 경우 Boot2Docker VM의 볼륨 공간이 부족하면 문제가 발생할 수 있습니다. 솔루션은 먼저 볼륨을 복제한 다음 디스크 파티셔닝 도구를 사용하여 크기를 조정하여 볼륨 크기를 늘리는 것입니다. (GParted)[ http://gparted.sourceforge.net/download.php/index.php ]는 무료 ISO이고 VirtualBox와 잘 작동하기 때문에 사용하겠습니다.
1. Boot2Docker의 VM을 중지합니다.
$ boot2docker stop
Boot2Docker는 VirtualBox의 기본 도구로 크기를 조정할 수 없는 VMDK 이미지와 함께 제공됩니다. 대신 VDI 볼륨을 만들고 VMDK 볼륨을 여기에 복제합니다.
2. VirtualBox 명령줄 도구를 사용하여 VMDK 이미지를 VDI 이미지로 복제합니다.
$ vboxmanage clonehd /full/path/to/boot2docker-hd.vmdk /full/path/to/<newVDIimage>.vdi —format VDI —variant Standard
3. 필요에 적합한 크기를 선택하여 새 복제 볼륨의 크기를 조정합니다. 많은 컨테이너를 회전하거나 컨테이너가 특히 큰 경우 더 큰 것이 좋습니다.
$ vboxmanage modifyhd /full/path/to/<newVDIimage>.vdi —resize <size in MB>
4. (GParted)[ http://gparted.sourceforge.net/download.php/ ] 와 같은 디스크 파티션 도구 ISO를 다운로드합니다 . Boot2Docker VM의 IDE 버스에 ISO를 추가합니다. ISO를 추가하기 전에 버스를 생성해야 할 수도 있습니다.
5. VirtualBox의 Boot2Docker 이미지에 새 VDI 이미지를 추가합니다.
6. Boot2Docker VM에 대한 설정에서 CD/DVD가 부팅 순서 목록 의 맨 위에 있는지 확인 합니다.
7. VirtualBox에서 Boot2Docker VM을 시작하면 디스크 파티션 ISO가 시작됩니다.
GParted를 사용하여 GParted Live(기본 설정) 옵션을 선택합니다. 기본 키보드, 언어 및 XWindows 설정을 선택하면 GParted 도구가 시작되고 생성한 새 VDI 볼륨이 표시됩니다. VDI를 마우스 오른쪽 버튼으로 클릭하고 크기 조정/이동 을 선택합니다 . 볼륨을 나타내는 슬라이더를 최대 크기로 끌어 크기 조정/이동 을 클릭 한 다음 적용 을 클릭 합니다. GParted를 종료하고 VM을 종료합니다. VirtualBox의 Boot2Docker VM용 IDE 컨트롤러에서 GParted ISO를 제거합니다.
8. VirtualBox에서 또는 명령줄( boot2docker start)을 사용하여 Boot2Docker VM을 시작하여 볼륨 변경 사항이 적용되었는지 확인합니다.
Apache에서 gzip을 이용한 압축전송하기
배경
1.브라우저에서 index.html을 서버에 요청합니다.
2.서버는 해당 파일을 찾습니다.
3.파일을 요청한 브라우저로 전송합니다(100KB).
4.브라우저는 해당 파일을 표시합니다.

1.브라우저에서 index.html을 서버로 요청시 gzip을 지원한다는 내용을 서버로 전달합니다.
2.서버는 해당 파일을 찾습니다.
3.gzip으로 파일을 압축해서 보냅니다(30KB).
4.브라우저에서 압축된 파일을 풀고 표시합니다.
테스트 환경
- 서버 OS: Windows7
- 웹서버: Apache 2.4
- 브라우저: IE9, 크롬, Firefox, Safari
1.Apache httpd.conf 파일 열기
2.LoadModule 영역에서 아래내용에 주석처리 되었으면 해제합니다.
LoadModule deflate_module modules/mod_deflate.so
LoadModule headers_module modules/mod_headers.so
LoadModule filter_module modules/mod_filter.so
<IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/plain AddOutputFilterByType DEFLATE text/html AddOutputFilterByType DEFLATE text/xml AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE application/xml AddOutputFilterByType DEFLATE application/xhtml+xml AddOutputFilterByType DEFLATE application/rss+xml AddOutputFilterByType DEFLATE application/javascript AddOutputFilterByType DEFLATE application/x-javascript DeflateCompressionLevel 9 BrowserMatch ^Mozilla/4 gzip-only-text/html # Netscape 4.xx에는 HTML만 압축해서 보냄 BrowserMatch ^Mozilla/4\.0[678] no-gzip # Netscape 4.06~4.08에는 압축해서 보내지 않음 BrowserMatch \bMSIE !no-gzip !gzip-only-text/html # 자신을 Mozilla로 알리는 MSIE에는 그대로 압축해서 보냄 ##예외 설정 SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|bmp|zip|gz|rar|7z)$ no-gzip dont-vary ####로그설정. ##DeflateFilterNote Input instream ##DeflateFilterNote Output outstream ##DeflateFilterNote Ratio ratio ## ##LogFormat '"%r" %{outstream}n/%{instream}n (%{ratio}n%%)' deflate ##CustomLog logs/deflate_log deflate
4.아파치를 restart 시킨 후 아래 사이트에서 gzip전송이 정상동작하는지 테스트 합니다.
http://www.whatsmyip.org/http-compression-test/

위 결과 페이지에서는 특정페이지가 84.5% 절감되고 있습니다.
각 라인 마지막 부분의 괄호안 퍼센트는 압축전 파일크기에 대한 압축 후 파일크기 비율을 나타냅니다. "GET /nexacro14lib/framework/SystemBase.js HTTP/1.1" 23987/176486 (13%) "GET /nexacro14lib/framework/Platform_HTML5.js HTTP/1.1" 18786/138984 (13%) "GET /nexacro14lib/framework/ErrorDefine.js HTTP/1.1" 4886/18684 (26%) "GET /nexacro14lib/framework/CssObjs.js HTTP/1.1" 25266/271916 (9%) "GET /nexacro14lib/framework/BasicObjs.js HTTP/1.1" 9152/50003 (18%) "GET /nexacro14lib/framework/Platform.js HTTP/1.1" 58726/370077 (15%) "GET /nexacro14lib/framework/SystemBase_HTML5.js HTTP/1.1" 50261/334443 (15%) "GET /nexacro14lib/component/CompBase/CompBase.js HTTP/1.1" 28574/167299 (17%) "GET /nexacro14lib/component/CompBase/CompEventBase.js HTTP/1.1" 17397/255753 (6%) 이하생략
Online YUI Compressor에 접속 후 아래그림 처럼 소스를 gz파일로 변환한다.① 원하는 소스를 붙여넣기 한다.② 결과는 “Redirect to gzipped output”을 선택한다.③ “Compress”버튼을 누르면 잠시 후 파일을 저장할 수 있는 팝업창이 표시된다.④ “gz”확장자로 원하는 위치와 파일명으로 저장합니다.

<script type="text/javascript" src="./pkg/EcoBasic.js.gz"></script>
Content-Encoding:gzip Content-Type:application/x-gzip Resource interpreted as Script but transferred with MIME type application/x-gzip: "http:// ~ /pkg/EcoBasic.js.gz".
LoadModule dir_module modules/mod_dir.so
AddEncoding x-compress .Z AddEncoding x-gzip .gz .tgz
#AddType application/x-compress .Z #AddType application/x-gzip .gz .tgz
# 경로는 root 이하 파일위치를 적어줌. <Directory /~/EcoPkg> Options Indexes MultiViews FollowSymLinks #AllowOverride None #Order deny,allow #Deny from all #Allow from 127.0.0.0/255.0.0.0::1/128 AddEncoding gzip gz <FilesMatch "\.gz$"> ForceType text/plain Header set Content-Encoding: gzip FilesMatch> ## <FilesMatch "\.css\.gz$"> ## ForceType text/css ## Header set Content-Encoding: gzip ## FilesMatch> Directory>
마치며…
- Netscape 6+ (Netscape 4-5 does, but with some bugs).
- Internet Explorer 5.5+ (July 2000) and IE 4 if set to HTTP/1.1.
- Opera 5+ (June 2000)
- Firefox 0.9.5+ (October 2001)
- Chrome since forever
- Safari since forever (as far as I can tell)
시놀로지 4TB디스크를 8TB디스크로 교체하기
Docker로 Hadoop 테스트 환경 구축하기 – HDFS
Docker와 테스트 환경
Docker는 애플리케이션을 컨테이너화해서 이미지로 만들어 배치하고 실행할 수 있는 환경을 제공하는 컨테이너 도구1이다.
격리된 파일 시스템을 갖는 컨테이너 특성 덕분에, 운영 환경에 대한 프로비져닝 뿐만 아니라 Docker가 설치된 환경에서 컨테이너화된 애플리케이션을 즉시 구동시켜서 테스트할 수 있는 환경을 구성하는데에도 유용하게 쓰인다.
오픈소스를 포함한 다양한 애플리케이션을 로컬 환경에 설치하고 구성하면서 테스트하다보면(주로 개발자의 노트북), 어느새 로컬 환경은 지저분해지고 몇 해 전에 설치한 애플리케이션이 자동 재시작되어 리소스를 점유하게 된다. 환경 설정 파일은 언제 어떻게 바꿨는지도 모른다. VM 환경으로 OS를 완전히 격리시켜 구성하자니 구동하는데 시간이 오래 걸리고 구동한 이후 점유하는 리소스가 노트북이 감당하기에는 참 크다.
Docker는 이런 환경에서 최적의 도구이다. 컨테이너를 어떻게 구성할지 애플리케이션마다 설정 파일을 생성하고, 서로 상호작용하는 애플리케이션인 경우 다시 실행과 관련된 설정 파일을 생성해서 여러 개의 애플리케이션을 순식간에 실행하고 다시 종료시킬 수 있다.
이 포스트에서는 Hadoop HDFS를 Docker로 컨테이너화 헤서 로컬에 테스트 환경으로 구성하는 방법을 설명한다.
HDFS
하둡 분산 파일 시스템(HDFS)은 마스터 노드인 NameNode와 워커 노드인 DataNode로 이루어져있다. 각각 JVM 위에서 동작하는 자바 애플리케이션으로, 두 애플리케이션만 동작을 하면 HDFS를 구성해서 읽고 쓸 수 있다.
실행 환경은 일반적은 GNU/Linux 그리고 Windows 환경을 지원하며, Hadoop 2.7~2.x2 버전은 Java 7, 8을 지원한다.3
클라이언트에서 각 애플리케이션과 직접 통신하는 포트 정보는 다음 표와 같다.
| 애플리케이션 | 기본포트 | 프로토콜 | 상세설명 | 관련 설정 항목 |
|---|---|---|---|---|
| NameNode | 50070 | HTTP | HDFS 상태 및 파일 시스템 조회용 Web UI | dfs.http.address |
| 9000 | IPC | 파일 시스템 메타데이터 관련 작업용 | fs.defaultFS |
|
| DataNode | 50075 | HTTP | 상태 및 로그 조회용 Web UI | dfs.datanode.http.address |
| 50010 | TCP | 데이터 전송 | dfs.datanode.address |
|
| 표 1. 클라이언트에서 HDFS와 직접 통신할 때 사용하는 포트 정보 |
이 자료들을 가지고 이제 본격적으로 Docker 컨테이너화를 진행해보자.
Dockerize
애플리케이션을 Docker에서 사용하는 설정 파일인 Dockerfile 양식에 맞게 구성한다. 이 설정 파일이 있으면 Docker 이미지 빌드가 가능하고, 생성한 이미지를 어디에서든 당겨와서(pull) 실행할 수 있다.
사실 이미지를 생성하는 것은 이 설정 파일 없이 직접 기반이 되는 OS 이미지로부터 컨테이너를 실행해서 쉘로 접속, 필요한 작업을 하고 이미지를 커밋해도 된다.
하지만 이 방식은 다음과 같은 문제점이 있다:
- 이미지가 어떻게 생성되었는지 기록이 남지 않음
- 따라서 작업한 내용을 다시 되돌리거나, 몇 가지 설정을 변경해서 새로운 이미지를 생성하기 어려움
- 그리고 생성된 이미지를 신뢰하기 어려움. 따라서 다른 사람과 공유하는데에도 적합하지 않음
블록을 쌓아 올리듯 이미지를 만드는 Docker 세계에서는, 처음에는 조금 번거로워 보이더라도 Dockerfile로 이미지를 만드는 것에 익숙해지는 것이 좋다.
Base
NameNode 이미지를 만들기 전에, 먼저 각 컴포넌트의 밑바탕이 되는 이미지를 구성한다. 밑바탕 이미지는 다음과 같은 내용을 포함헤야 할 것이다.
- Hadoop binary
- Java
- Common configurations
여기에서는 현재 2019년 8월 기준 Hadoop 2버전의 최신 버전인 2.9.2 버전을 사용했다. Hadoop 2는 Java 7 이상을 지원하므로 Java 8을 선택했다. 마지막으로 공용 설정은 core-site.xml, hdfs-site.xml을 포함하도록 구성할 것이다.
먼저 다음과 같은 디렉토리 구조로 폴더와 파일을 생성한다.
.
└── hadoop
├── base
│ ├── Dockerfile
│ └── core-site.xml
├── namenode
│ ├── Dockerfile
│ ├── hdfs-site.xml
│ └── start.sh
└── datanode
├── Dockerfile
├── hdfs-site.xml
└── start.sh
hadoop의 하위 폴더인 base, namenode, 그리고 datanode 폴더와 그 하위에 다시 위치한 개별 Dockerfile은 각 항목이 이미지로 빌드될 것임을 보여준다.
baes/Dockerfile의 내용을 다음과 같이 작성한다.
# 기반이 되는 이미지를 설정. 여기서는 ubuntu 18 기반인 zulu의 openjdk 8버전을 선택
FROM azul/zulu-openjdk:8
# 바이너리를 내려받기 위해 설치
RUN apt-get update && apt-get install -y curl
ENV HADOOP_VERSION=2.9.2
ENV HADOOP_URL=http://mirror.apache-kr.org/hadoop/common/hadoop-$HADOOP_VERSION/hadoop-$HADOOP_VERSION.tar.gz
# Hadoop 2.9.2 버전을 내려받고 /opt/hadoop에 압축 해제
RUN curl -fSL "$HADOOP_URL" -o /tmp/hadoop.tar.gz \
&& tar -xvf /tmp/hadoop.tar.gz -C /opt/ \
&& rm /tmp/hadoop.tar.gz
# 데이터 디렉토리 생성 및 설정 폴더의 심볼릭 링크 생성
RUN ln -s /opt/hadoop-$HADOOP_VERSION /opt/hadoop \
&& mkdir /opt/hadoop/dfs \
&& ln -s /opt/hadoop-$HADOOP_VERSION/etc/hadoop /etc/hadoop \
&& rm -rf /opt/hadoop/share/doc
# 로컬의 core-site.xml 파일을 복제
ADD core-site.xml /etc/hadoop/
# 실행 환경에 필요한 환경 변수 등록
ENV HADOOP_PREFIX /opt/hadoop
ENV HADOOP_CONF_DIR /etc/hadoop
ENV PATH $HADOOP_PREFIX/bin/:$PATH
ENV JAVA_HOME /usr/lib/jvm/zulu-8-amd64
기반 이미지인 azul/zulu-openjdk:8는 ubuntu 18 기반이라서 이미지 크기가 315MB로 꽤 크다.
이 크기를 작게 줄이려고 alpine 기반으로 생성해서는 안된다. Hadoop은 일반적인 GNU/Linux 환경에서 동작할 수 있도록 개발되었기 때문이다.
base/core-site.xml은 다음과 같이 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://namenode:9000/</value>
<description>NameNode URI
</description>
</property>
</configuration>
fs.defaultFS 설정은 NameNode 위치를 찾는 중요한 설정으로, DataNode 뿐만 아니라 각종 애플리케이션에서 NameNode에 파일 읽기/쓰기 요청을 할 때 사용되는 항목이다.
URI의 호스트 이름이 namenode로 설정되었는데, NameNode 컨테이너의 호스트 이름을 namenode로 지정할 것이다.
이제 터미널에서 base 디렉토리로 이동해서 baes 이미지를 생성해보자.
cd base
docker build -t hadoop-base:2.9.2 .
현재 위치한 폴더의 Dockerfile을 사용해서 docker 이미지를 빌드하겠다는 명령어이다. Dockerfile을 작성한대로 각 라인이 하나의 빌드 단계가 되어 명렁어를 실행하는 로그가 출력된다.
빌드가 완료되면 다음 명렁어로 로컬에 생성된 이미지를 확인할 수 있다.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hadoop-base 2.9.2 fab79eab7d71 2 mins ago 1.2GB
이 이미지는 다음과 같은 이미지 계층 구조를 갖는다. 간단한 구조를 갖지만 이 base 이미지를 통해 모든 Hadoop 컴포넌트의 기반으로 사용할 수 있다.
그림 1. Hadoop base 이미지 계층 구조. OS → Runtime → App base의 간단한 구조이다.
NameNode
Java와 Hadoop binary를 가지고 있는 base 이미지를 생성했으니, 이제 NameNode 이미지를 생성해보자.
NameNode 구성시 고려해야할 부분은 다음과 같다.
- FsImage, EditLog를 저장하는 로컬 파일 시스템 경로
- NameNode용
hdfs-site.xml설정 - NameNode 최초 구동 확인 및 네임스페이스 포맷
먼저 hdfs-site.xml 파일을 다음과 같이 작성한다.
dfs.namenode.name.dir은 FsImage, EditLog 파일을 저장하는 경로이다. HDFS 파일 블록 크기를 결정하는 dfs.blocksize 항목은 바이트 수로 여기서는 테스트를 위해 기본 값보다 훨씬 작은 10MB로 설정했다. (기본값은 128MB) 이 외의 항목들은 hdfs-default.xml 파일을 참고한다.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:///opt/hadoop/dfs/name</value>
</property>
<property>
<name>dfs.blocksize</name>
<value>10485760</value>
</property>
<property>
<name>dfs.client.use.datanode.hostname</name>
<value>true</value>
</property>
<property>
<name>dfs.namenode.rpc-bind-host</name>
<value>0.0.0.0</value>
</property>
<property>
<name>dfs.namenode.servicerpc-bind-host</name>
<value>0.0.0.0</value>
</property>
<property>
<name>dfs.namenode.http-bind-host</name>
<value>0.0.0.0</value>
</property>
<property>
<name>dfs.namenode.https-bind-host</name>
<value>0.0.0.0</value>
</property>
</configuration>
시작 스크립트인 start.sh에서는 NameNode의 네임스페이스가 포맷되었는지 확인하고, 포맷되기 전이라면 구동 전 포맷을 먼저 진행해야 한다.
다음과 같이 작성했다.
#!/bin/bash
# 네임스페이스 디렉토리를 입력받아서
NAME_DIR=$1
echo $NAME_DIR
# 비어있지 않다면 이미 포맷된 것이므로 건너뛰고
if [ "$(ls -A $NAME_DIR)" ]; then
echo "NameNode is already formatted."
# 비어있다면 포맷을 진행
else
echo "Format NameNode."
$HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR namenode -format
fi
# NameNode 기동
$HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR namenode
Dockerfile은 base 이미지 덕분에 간단하다.
# 방금 전 로컬에 생성한 base 이미지
FROM hadoop-base:2.9.2
# NameNode Web UI 응답 여부를 통해 Healthcheck
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD curl -f http://localhost:50070/ || exit 1
# 설정 파일 복제
ADD hdfs-site.xml /etc/hadoop/
# FsImage, EditLog 파일 경로를 volume으로 연결
RUN mkdir /opt/hadoop/dfs/name
VOLUME /opt/hadoop/dfs/name
# 실행 스크립트 복제
ADD start.sh /start.sh
RUN chmod a+x /start.sh
# NameNode의 HTTP, IPC 포트 노출
EXPOSE 50070 9000
# 시작 명령어 등록
CMD ["/start.sh", "/opt/hadoop/dfs/name"]
이제 이미지를 생성하자. 기본 명령어와 바이너리 파일을 다운로드 받는 등 시간이 오래 걸리는 작업은 이미 base 이미지에서 처리했기 때문에, 그 위에 다시 한번 계층을 쌓는 NameNode는 빌드 시간이 얼마 걸리지 않는 것을 볼 수 있다.
cd namenode
docker build -t hadoop-namenode:2.9.2 .
두 개의 이미지가 생성되었고 계층 구조에 하나의 이미지가 더해졌다.

NameNode 이미지를 생성했으니 이제 실제로 기동시켜볼 차례이다. 어차피 DataNode 이미지를 생성하면 docker compose를 구성하는 것이 훨씬 간편하므로 바로 compose 설정 파일을 구성하겠다.
먼저 root 디렉토리에 docker-compose.yml 파일을 생성한다. 디렉토리 구조를 보면 아래와 같다.
.
└── hadoop
├── base
├── namenode
├── datanode
└── docker-compose.yml
설정 파일의 내용을 작성한다. services에는 기동할 컨테이너 정보를, volumes에는 마운트할 volume 정보를, networks에는 컨테이너 간 어떤 네트워크 구성을 사용할지 지정하는 항목이다.
volumes 이하에 정의된 각 항목은 다음과 같은 의미를 갖고 있다.
namenode:/opt/hadoop/dfs/name:namenodevolume을 컨테이너의/opt/hadoop/dfs/name경로에 마운트/tmp:/tmp: 컨테이너가 기동되는 호스트의/tmp경로를 컨테이너의/tmp경로로 마운트. 호스트와 컨테이너 간 파일을 쉽게 공유하기 위해서 지정함
콜론으로 구분된 설정 값의 뒷 부분이 컨테이너에 해당한다는 것을 기억하면 쉽게 이해할 수 있다.
version: "3"
services:
namenode:
image: hadoop-namenode:2.9.2
container_name: namenode
hostname: namenode
ports:
- "50070:50070"
- "9000:9000"
volumes:
- namenode:/opt/hadoop/dfs/name
- /tmp:/tmp
networks:
- bridge
volumes:
namenode:
networks:
bridge:
이제 이 설정 파일을 바탕으로 NameNode 컨테이너를 기동할 수 있다.
# docker-compose.yml 파일이 위치한 경로로 이동
cd hadoop
# 컨테이너를 백그라운드에서 실행
# 종료할 때에는 docker-compose down
docker-compose up -d
Creating namenode ... done
# 기동중인 컨테이너 프로세스 확인
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS 622e4f642435
hadoop-namenode:2.9.2 "/start.sh /opt/hado…" 10 seconds ago Up 6 seconds (health: starting) 0.0.0.0.:9000->9000/tcp, 0.0.0.0.:50070->50070/tcp
# 생성된 bridge 네트워크 확인
docker network ls
NETWORK ID NAME DRIVER SCOPE
0d9989bd9f78 bridge bridge local
9ae1ae84360f hadoop_bridge bridge local
62a94ebd58b7 host host local
# 생성된 volume 확인
docker volume ls
DRIVER VOLUME NAME
local hadoop_namenode
# namenode 컨테이너의 로그 확인
docker logs -f namenode
docker compose로 기동한 후 마지막 명령어로 로그를 살펴보면, 네임스페이스를 포맷한 로그와 NameNode를 기동한 로그를 확인할 수 있다.
브라우저에서 http://localhost:50070 으로 접속하면 아래와 같은 NameNode Web UI에 접근한다.

또한 NameNode가 기동되었으니, 아직 DataNode를 띄우지 않더라도 폴더의 생성과 삭제, 그리고 파일 목록 조회가 가능하다.
NameNode 컨테이너 내부에 있는 Hadoop 클라이언트를 실행해서 테스트해보자.
# namenode 이름의 컨테이너의 hadoop 클라이언트를 실행. 파일 시스템의 root 디렉토리를 모두 조회한다.
docker exec namenode /opt/hadoop/bin/hadoop fs -ls -R /
# 명령어 등록
alias hadoop="docker exec namenode /opt/hadoop/bin/hadoop"
# 폴더 생성/조회/삭제
hadoop fs -mkdir -p /tmp/test/app
hadoop fs -ls -R /tmp
hadoop fs -rm -r /tmp/test/app
docker exec [컨테이너 이름] [명령어]를 입력하면 동작 중인 컨테이너 내에서 [명령어]에 해당하는 명령어를 실행한 결과의 표준 출력을 현재 쉘에 출력한다.
컨테이너 내 자주 실행하는 프로그램응 alias로 등록해두면 편리하다. 여기에서는 NameNode의 bin/hadoop 프로그램을 hadoop으로 alias 지정했다.
DataNode
먼저 작성했던 JVM과 하둡 바이너리를 포함하는 base 이미지로부터 DataNode 이미지를 생성해보자. 고려해야할 부분은 두 가지이다.
- 파일 블록을 저장하는 로컬 파일 시스템 경로
- DataNode용
hdfs-site.xml설정
파일 블록 저장 경로는$HADOOP_PREFIX/dfs/data로 설정할 것이다. 이 내용은hdfs-site.xml파일을datanode디렉토리 이하에 생성하고 내용을 다음과 같이 작성한다.
<configuration>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:///opt/hadoop/dfs/data</value>
</property>
<property>
<name>dfs.blocksize</name>
<value>10485760</value>
</property>
<property>
<name>dfs.datanode.use.datanode.hostname</name>
<value>true</value>
</property>
</configuration>
시작 스크립트는 start.sh에 다음과 같이 작성한다.
#!/bin/sh
$HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR datanode
hdfs 프로그램을 datanode 옵션으로 시작하면 DataNode 프로세스가 기동한다.
마지막으로 Dockerfile 이다. HEALTHCHECK에 Web UI 주소가 입력된 것과 EXPOSE에 명시한 포트를 눈여겨 본다.
FROM hadoop-base:2.9.2
# DataNode Web UI 응답 여부를 통해 Healthcheck
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD curl -f http://localhost:50075/ || exit 1
RUN mkdir /opt/hadoop/dfs/data
VOLUME /opt/hadoop/dfs/data
ADD start.sh /start.sh
RUN chmod a+x /start.sh
# WebUI, 데이터전송
EXPOSE 50075 50010
CMD ["/start.sh"]
NameNode 이미지를 구성할 때와 크게 다르지 않다. 이제 생성한 파일들을 가지고 이미지를 생성한다.
cd datanode
docker build -t hadoop-datanode:2.9.2 .
이제 컨테이너 실행 환경 정보를 담고 있는 docker-compose.yml 파일에 DataNode를 추가한다. 로컬 환경이지만 세 개의 DataNode를 띄울 것이다.
version: "3.4"
# 이미지와 네트워크 정보에 대한 base service를 지정
x-datanode_base: &datanode_base
image: hadoop-datanode:2.9.2
networks:
- bridge
services:
namenode:
image: hadoop-namenode:2.9.2
container_name: namenode
hostname: namenode
ports:
- 50070:50070
- 9000:9000
volumes:
- namenode:/opt/hadoop/dfs/name
- /tmp:/tmp
networks:
- bridge
datanode01:
<<: *datanode_base
container_name: datanode01
hostname: datanode01
volumes:
- datanode01:/opt/hadoop/dfs/data
datanode02:
<<: *datanode_base
container_name: datanode02
hostname: datanode02
volumes:
- datanode02:/opt/hadoop/dfs/data
datanode03:
<<: *datanode_base
container_name: datanode03
hostname: datanode03
volumes:
- datanode03:/opt/hadoop/dfs/data
volumes:
namenode:
datanode01:
datanode02:
datanode03:
networks:
bridge:
파일을 수정하고 docker-compose up -d를 다시 실행하면 세 개의 DataNode 컨테이너를 추가로 띄우게 된다. DataNode는 NameNode로 heartbeat을 보내면서 NameNode에 등록된다.
Elastic Search Index and Shade Split point
여러개의 샤드 vs 인덱스 쪼개기
다나와 서비스의 상품갯수는 급격하게 늘어나고 있습니다. 몇달전 4~5억건이었던 상품이 7억건에 육박하고 있는데요. 현재 사용하고 있는 패스트캣 검색엔진에서는 대량의 문서들을 여러개의 인덱스로 나누어 색인하고 있습니다.
검색할때는 나눈 인덱스들을 하나로 합쳐서 검색하고 있는데요. 엘라스틱서치에도 여러 인덱스를 하나로 합쳐서 한번에 검색하는 기능이 있어서 지금처럼 인덱스를 쪼개서 사용하는 것도 가능한 시나리오입니다. 참고로 얘기하면, 이렇게 데이터를 범위나 카테고리로 나누어 관리하는 기법을 파티셔닝이라고 합니다.
먼저 인덱스를 하나로 가져가든 여러개로 나누든, 샤드 하나의 크기는 비슷하게 설정해야 할텐데요, 적절한 샤드의 크기를 먼저 정해야 할 것 같습니다. 몇차례 구글링을 한결과 엘라스틱서치의 공식블로그에서 도움이 될만한 글를 찾을수 있었습니다.
TIP: 작은 샤드는 작은 세그먼트를 만들며 부하를 증가시킵니다. 평균 샤드 크기를 최소한 수 GB와 수십 GB 사이를 유지하세요. 시간 기반 데이터를 사용한 과거 사례를 보면, 20GB ~ 40GB 정도의 사이즈가 적당합니다.
샤드크기는 수GB에서 수십GB 사이가 적당하며, 경험상 시계열 데이터의 경우 20GB~40GB가 적당하다고 합니다. 그리고 아래에서 또다른 팁을 발견할수 있었습니다.
TIP: 하나의 노드에 저장할 수 있는 샤드의 개수는 가용한 힙의 크기와 비례하지만, Elasticsearch에서 그 크기를 제한하고 있지는 않습니다. 경험상 하나의 노드에 설정한 힙 1GB 당 20개 정도가 적당합니다. 따라서 30GB 힙을 가진 노드는 최대 600개 정도의 샤드를 가지는 것이 가능하지만, 이 보다는 적게 유지하는 것이 더 좋습니다.
힙메모리 30GB로 엘라스틱서치를 구동시 샤드는 최대 600개 정도라고 합니다. 이미 32GB 를 힙에 할당할 생각을 하고 있으므로, 활성화된 인덱스가 50개라고 한다면, 인덱스당 600샤드 / 50 인덱스 = 12 로 인덱스당 최대 12개의 샤드를 설정할수 있을 겁니다. 일단 샤드갯수의 최대치는 넉넉한것 같으므로, 많아서 문제가 될것같지는 않습니다.
하나의 샤드를 20GB~40GB로 설정한다면, 7억건의 상품은 몇개의 샤드로 나눠지게 될까요? 다나와 상품기준으로는 15개의 샤드로 나눠지게 됩니다. 물론 엘라스틱 서치 블로그에서는 시계열 데이터에 대한 팁이라서 상품과는 문서의 특성이 다릅니다. 대부분은 웹서버나 DB 로그데이터 일텐데요, 로그스태시의 파싱모듈을 확인해보면 한 로그내의 필드수가 10개 내외로 매우 적습니다. 반면에 상품의 필드는 수십개는 기본입니다.
이러한 문서특성의 차이로 검색과 색인시 한개 문서에 들어가는 리소스와 시간이 더 투여됩니다. 그러므로, 상품문서는 하나의 샤드를 20GB 보다는 조금 더 작게 나누면 여러 서버에 더 잘게 분산이 되어 전체적인 응답시간은 더 빨라질겁니다. 우리는 샤드당 약 5GB로 설정하고 테스트 했는데, 빠른 응답속도를 얻을 수 있었습니다.
그럼, 인덱스를 나누는것과 샤드를 나누는것 어떤것이 적합할까요? 어치피 샤드는 여러 서버에 분산되어 병렬 및 병행 (참고: https://soy.me/2015/01/03/concurrent/)으로 검색이 되므로, 인덱스가 같던 다르던 상관은 없습니다. 검색의 기본 단위는 샤드이기 때문이죠. 따라서 인덱스를 나누는 것은 운영의 편의성을 고려할때 선택하는 방법입니다. 장애없는 운영의 측면에서 생각해보면, 큰 덩어리 하나를 다루는 것은 부담스러워도, 작은 덩어리 여러개를 다루는 것이 번거롭긴해도 그만큼 장애 가능성을 낮추는 방법이 됩니다.
전체색인을 빠르게 하려면
전체색인을 할 경우 인덱스 하나가 7억건이라면 색인이 모두 끝날때까지 약 3-4시간이 소요될것이고, 검색에 노출되기 까지 이시간을 고스란히 기다려야 합니다. 더 문제가 되는것은 전체색인 도중에도 상품의 가격은 계속해서 변하게 되는데 이 변경분을 전체색인후에 일괄적용을 해야하며, 대기시간이 길수록 일괄적용시간도 늘어나게 됩니다. 그러므로, 최대한 전체색인을 빠르게 해야하는데, 이를 위해서는 문서를 입력하는 REST 클라이언트를 멀티스레드로 여러개 생성하여 동시입력량을 늘려야 합니다. 이때 Bulk Insert 를 쓰는것은 기본이구요.
하지만, 엘라스틱서치의 Write IO도 고려해야 합니다. 동시입력량을 늘리면, 서버의 CPU와 Disk IO중 둘중 하나는 최대치에 이를것이고, 더이상 색인속도는 늘어나지 않게 됩니다. 우리가 경험한 최대 색인속도는 500KB 상품문서 기준으로 16000건/초 였습니다. 이때 CPU는 32코어로 90%를 사용했으니 활용도는 매우 높다고 할수 있습니다. (참고: https://danawalab.github.io/elastic/2020/07/06/Elasticsearch-Index.html)
결국은 더 빠른 색인을 위해서는 하나의 인덱스를 여러개로 나눠야 합니다. 통으로 4시간이 걸리는 문서를 10개의 인덱스로 나누면 색인시간이 서버를 공유하므로 정확히 1/10이 될수는 없지만, 그대로 각각 40분정도로 병행완료가 됩니다.
상품DB의 특성상 우리는 카테고리군별로 인덱스를 나누고 있습니다. 카테고리별로 인덱스를 나눌때의 장점은 특정카테고리만 검색할때 해당 인덱스만 검색하면 되므로, 검색부하가 현저히 감소하게 됩니다. 우리는 인덱스도 나누고 샤드도 2-3개로 나누어 전체적인 검색응답시간을 최대한 단축하는 방향으로 설계했습니다.
레플리카 갯수는 몇개로?
고가용성을 추구할때 빼놓을 수 없는 것이 레플리카인데요, 레플리카는 레플리카 샤드를 줄여서 얘기하는 것으로, 프라이머리 샤드에 종속됩니다. 우리말로는 복제본 이라고도 하죠. 복제본은 분산 데이터 시스템에서는 동일한 역할을 담당하는데요, 하드웨어 장애를 극복하고 검색과 같은 읽기처리량을 향상시키는 것입니다.
그런데 읽기성능이 좋아진다는 것은 쓰기성능이 낮아진다는 얘기도 됩니다. 왜냐하면, 복제본을 여러개 만들기 위해서는 그만큼 문서색인시에 쓰기작업도 복제본 갯수만큼 발생하기 때문이죠. 하나의 서버만 본다면 1이지만, 전체클러스터 입장에서는 복제본이 열개면 10의 쓰기가 일어나는 것입니다. 문서가 색인될때 구지 모든 서버의 IO발생하게 만들 필요는 없을겁니다.
또한 복제본의 갯수는 동시에 장애가 발생할 노드를 몇개 까지 허용하는지에 달려있습니다. 복제본이 1개라면 하나의 노드가 죽어도 검색서비스는 유지됩니다. 하지만, 이제 복제본은 0개가 되므로, SA가 빨리 노드를 복구하지 않으면, 다른 또하나의 노드가 죽을때 검색서비스에는 장애가 생깁니다.
만약 회사내에 장애 복구시스템이 잘 갖춰져 있다고 하면, 복제본은 1개로도 가능합니다. 복제본이 2라면 서버가 연속 2개 죽어도 상관이 없으므로, 마음을 느긋하게 가질수 있습니다. 하지만 전체 클러스터에서 서버가 2개 빠지고도 부하를 충분히 견딜수 있게 서버를 배치해 놓아야 합니다. 우리는 카테고리별 인덱스의 읽기성능을 고려해서 부하가 높은 인덱스는 복제본 2를 나머지는 1을 설정할 계획입니다.
결론
nginx로그와 같은 로그성 문서는 색인을 하고 나면 수정이 필요없는 정적인 컨텐츠인 반면에, 상품문서는 색인이 끝나도 계속 갱신이 되어야 하는 살아있는 컨텐츠입니다. 그렇기 때문에, 동적색인이 원활하고 장애도 대비할수 있으며, 검색성능도 높은 설계가 필요합니다. 그리고 엘라스틱서치를 사용하여 검색시스템을 설계할때 운영자가 할수 있는 최선의 방법은 문서와 샤드를 잘 관리하는 것입니다. 따라서 견고한 검색시스템을 위해 인덱스와 샤드를 어떻게 설정해야 하는지에 대해 살펴봤습니다.
참고
https://www.elastic.co/kr/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster
https://medium.com/kariyertech/elasticsearch-cluster-sizing-and-performance-tuning-42c7dd54de3
https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html
Kubernetes Virtual Storage (Ceph) Installation
설치에 필요한 도구로 Rook을 사용할 예정입니다. Rook은 오픈소스 클라우드 네이티브 스토리지 오케스트레이터로, 클라우드 네이티브 환경과 기본적으로 통합할 수 있는 다양한 스토리지 솔루션 세트에 대한 플랫폼, 프레임 워크 및 지원합니다. Rook을 통해 Ceph 가상스토리지를 구성하고 공유 파일 시스템을 적용하도록 하겠습니다.
Rook 구성도
Rook은 쿠버네티스 POD에서 실행되며, Ceph, EdgeFS등 가상솔루션을 POD로 배포하여 관리하는 도구입니다. agent를 통해 Ceph, EdgeFS등 가상솔루션을 관리하고, OSD를 통해 데이터를 영구저장합니다.

Ceph vs EdgeFS
Rook에서 제공하는 가상스토리지는 EgdeFS와 Ceph 외에도 다양하게 지원하지만 안정적인 버전은 아래 두가지만 지원합니다.
- EdgeFS: 데이터베이스처럼 대용량 스토리지가 필요할때 사용됩니다.
- Ceph: 공유 확장에 특화되어 있는 스토리지가 필요할때 사용됩니다.
공유나 확장에 특화되어 있는 Ceph를 설치하도록 하겠습니다.
Ceph 스토리지 유형
Ceph는 Block, Object, File 기반으로 데이터를 사용할 수 있습니다. 각 유형에 따라서 사용하는 기능에 차이가 있습니다.
- Block Stroage: 단일 POD에 storage 제공합니다.
- Object Storage: 애플리케이션이 쿠버네티스 클러스터 내부 또는 외부에서 액세스 할수있느 데이터를 IO 할수있고, S3 API를 스토리지 클러스터에 노출을 제공합니다.
- Shared Stroage: 여러 POD에서 공유할 수있는 파일 시스템기반 스토리지입니다.
Shared Storage 구성도
이번에 설치해볼 Shared Storage 구성입니다. Ceph 클러스터에 데이터를 저장하고 Ceph를 통해 POD들과 데이터가 공유가 됩니다.

Ceph 설치
테스트 환경
쿠버네티스가 설치된 환경이 필요합니다. 설치방법은 쿠버네티스 고가용성 클러스터 구성 블로그를 통해 설치진행 바랍니다.
OS: ubuntu:18.04
Kubernetes: 1.16.4
Ceph 소스 다운로드 및 POD 배포
Rook은 별도의 설치가 없고 github으로 Ceph, EdgeFS 가상스토리지 솔루션을 쿠버네티스에 배포할 수 있도록 제공하고 있습니다.
$ git clone --single-branch --branch release-1.2 https://github.com/rook/rook.git
아래 경로에 보면 common.yaml, operator.yaml이 있습니다. kubectl 명령어를 통해 배포 합니다.
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/common.yaml
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/operator.ymal
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/cluster.yaml
cluster.yaml을 배포하게 되면 POD 중 OSD가 Running 상태인지 확인이 필요합니다. 변경되는 시간이 약 1 ~ 3 분 정도 소요됩니다.
테스트 환경에 따라 OSD 갯수가 차이가 있을 수 있습니다.
$ kubectl get pod -n rook-ceph

OSD Running 상태까지가 ceph 클러스터 구성이 완료되었고, Shared Storage 유형을 적용해보겠습니다.
Shared Storage 유형 적용
아래 명령어를 실행하게 되면 MDS POD가 배포가 됩니다. 약 1 ~ 3분 정도 소요됩니다. Shared Storage 옵션을 변경할 수 있는데 자세한 내용은 ceph-filesystem-crd 통해 확인바랍니다.
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/filesystem.yaml
StorageClass Driver 적용
StorageClass는 POD에서 Ceph 데이터 풀에 접근하기 위해 사용되는 드라이버입니다. kubectl 명령어를 통해 적용합니다.
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/csi/cephfs/storageclass.yaml
Shared Storage 확인
정상적으로 적용이 되었는지 확인하는 방법압니다. 아래 결과 처럼 myfs가 생성되어 있습니다.
$ kubectl get CephFileSystem -A
결과)

Shared Storage 테스트
kube-registry를 배포하면 PVC, docker-registry POD가 배포됩니다. 컨테이너 내부에 접근하여 /var/lib/registry 디렉터리에 dummy 파일을 생성 하여 다른 POD에서 공유되어 보여지는지 확인합니다.
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/csi/cephfs/kube-registry.yaml
정리
Rook이라는 도구를 이용해서 Ceph를 설치해보았습니다. Ceph를 관리하려면 러닝커브가 있지만 Rook이라는 도구를 이용해서 Ceph 관리와 쿠버네티스와 연동을 쉽게 할 수 있습니다.
Configuring Kubernetes High Availability Clusters
소개
쿠버네티스는 마스터 노드에 내부적으로 API-SERVER를 가지고 있어 마스터 노드를 통해 워커 노드로 통신이 전달됩니다. 대량에 통신량이 발생하게 되면 마스터 노드는 부하를 많이 받아 장애 발생 가능성이 커지게 됩니다. 마스터 노드를 고가용성 클러스터로 연결하게 되면 통신량을 분산시킬 수 있고, 일부가 마스터 노드에서 장애가 발생시 워커 노드의 운영 시스템에는 영향을 줄일 수 있습니다.
구성 환경
아래 사양으로 총 3대 서버를 준비합니다.
OS: CentOS 7
CPU: 2
RAM: 4
Storage: 100G
구성목표
- 마스터 노드 3개를 클러스터구성
- 가상 네트워크(weave) 적용
- ceph storage 적용
도커&쿠버네티스 설치
CentOS 패키지 업데이트와 방화벽을 정지 및 스왑을 종료합니다.
$ sudo yum update -y
$ sudo systemctl disable firewalld && sudo systemctl stop firewalld
$ sudo selinux disabled
$ sudo setenforce 0
$ sudo iptables piv4 forward
$ sudosudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
$ sudo swapoff -a
$ sudo sed -i '/swap/d' /etc/fstab
운영 환경에서는 아래 포트를 오픈하여 진행합니다.
| 노드 | 포트 | TCP/UDP |
|---|---|---|
| 마스터 | 6443, 2379-2380, 10250, 10251, 10252 | TCP |
| 마스터, 워커 | 6783 | TCP |
| 마스터, 워커 | 6783, 6784 | UDP |
| 워커 | 10250, 30000-32767 | TCP |
| 로드 밸런서 | 26443 | TCP |
호스트네임 설정
호스트네임은 서로 다르게 지정해야합니다.
$ sudo hostnamectl set-hostname node1
$ sudo hostnamectl set-hostname node2
$ sudo hostnamectl set-hostname node3
도커 설치
도커 레파지토리를 등록하여 yum 으로 설치 합니다.
$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum install docker-ce docker-ce-cli containerd.io
$ sudo systemctl start docker
설치 후 daemon.json을 추가하여 cgroupdriver를 systemd 옵션으로 사용할 수 있도록 합니다.
$ sudo cat <<EOF > /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
]
}
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl enable docker
$ sudo systemctl restart docker
쿠버네티스 설치
쿠버네티스의 레파지토리 저장소를 등록하여 yum 으로 kubelet, kubeadm, kubectl을 설치합니다.
- kubeadm: 클러스터 관련 작업시 사용됩니다.
- kubectl: 클러스터의 서비스 및 pod 관리시 사용됩니다.
- kubelet: 클러스터 에이전트역할을 합니다.
$ sudo cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg [https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg](https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)
EOF
$ sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
$ sudo systemctl enable kubelet
$ sudo systemctl restart kubelet
node1, node2, node3에 도커와 쿠버네티스가 설치를 하였다면 아래 명령어를 통해 확인해볼 수 있습니다.
$ sudo docker verison
$ kubectl version
로드밸런서 설치
공식문서에서 사용되고 있는 HAProxy를 사용하도록 하겠습니다.
테스트환경이기 때문에 keepalive및 failover 기능없이 진행하도록 하겠습니다.
노드1에만 haproxy 를 설치하도록 하겠습니다.
$ sudo yum install haproxy -y
haproxy 로드밸런싱 설정
node1 IP의 26443포트로 전달받은 데이터를 node1 ~ node3의 6443 포트로 포워드 시켜줍니다.
라운드로빈으로 순차적으로 접근하도록 하겠습니다.
$ sudo cat <<EOF >> /etc/haproxy/haproxy.cfg
frontend kubernetes-master-lb
bind 0.0.0.0:26443
option tcplog
mode tcp
default_backend kubernetes-master-nodes
backend kubernetes-master-nodes
mode tcp
balance roundrobin
option tcp-check
option tcplog
server node1 <node1 IP>:6443 check
server node2 <node2 IP>:6443 check
server node3 <node3 IP>:6443 check
EOF
$ sudo systemctl restart haproxy
haproxy 확인
해당 포트가 오픈되어 있는지 확인합니다.
포트가 오픈되어 있어도 접근이 안될때는 방화벽을 확인해야 합니다.
netstat -an|grep 26443
클러스터 생성
클러스터 생성할때 –upload-certs, –control-plane-endpoint 플래그를 추가해야 인증서가 자동 배포되고, 마스터 노드 조인 명령어가 출력됩니다.
$ sudo kubeadm init --control-plane-endpoint "<노드1 DNS/IP or LoadBalancer DNS/IP>:26443" \
--upload-certs \
--pod-network-cidr "10.244.0.0/16"
config 생성
kubectl 사용하기 위해서는 사용자 디렉토리 하위에 .kube/config 가 필요합니다. kubeadm init 명령어를 진행하고 마지막에 로그를 잘보면 config를 생성하는 명령어가 적혀있습니다.
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
클러스터 연결
노드1에서 클러스터 생성시 kubeadm join 명령어가 출력되는데 –control-plane –certificate-key 플래그 포함하여 명령어를 실행하면 마스터 노드로 연결이 되고, 플래그는 추가하지 않고 사용하게 되면 워커노드로 연결됩니다.
노드2 클러스터 연결
$ sudo kubeadm join 192.168.248.251:26443 --token 06glbc.s0yaqonyajs95ez3 \
--discovery-token-ca-cert-hash sha256:379ff0daa2cffa3f6581ae25c96aa3d6e4a9af43df92b8d0ac5a4dbb4c7e5547 \
--control-plane --certificate-key 8eb7fa9a38ca1579c3fb61e0a1106935c4e9dfd81cbad259121f223f0adf555f
config 생성
노드2의 사용자에서도 kubectl 명령어를 사용하기 위해선 config를 생성해야 합니다.
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
노드3 클러스터 연결
노드3에서도 노드2와 동일하게 연결을 진행합니다.
$ sudo kubeadm join 192.168.248.251:26443 --token 06glbc.s0yaqonyajs95ez3 \
--discovery-token-ca-cert-hash sha256:379ff0daa2cffa3f6581ae25c96aa3d6e4a9af43df92b8d0ac5a4dbb4c7e5547 \
--control-plane --certificate-key 8eb7fa9a38ca1579c3fb61e0a1106935c4e9dfd81cbad259121f223f0adf555f
config 생성
노드3에서도 동일하게 config를 생성해야합니다.
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
클러스터 구성 확인
노드가 연결이 되었다면 아래 명령어를 실행시 노드가 전부 보여야합니다.
$ kubectl get node
마스터 노드에는 기본설정으로 pod 배포가 안됩니다. taint명령어를 통해 pod 배포가 가능하도록 하겠습니다.
$ kubectl taint nodes --all node-role.kubernetes.io/master-
가상 네트워크(weave) 적용
Calico, Canal, Clilum, Flannel, weave 등 다양한 가상네트워크가 있지만 공식문서에서 제공하는 weave를 적용하도록 하겠습니다.
$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
가상네트워크 적용 확인
최초 클러스터를 진행하게 되면 노드의 상태는 NotReady 입니다. 가상네트워크를 적용하게 되면 약 1 ~ 3분 소요되고 Ready 상태로 변경되게 됩니다. 이것으로 가상네트워크 적용이 완료 되었습니다.
$ kubectl get node
nginx 배포 해보기
nginx pod 3개를 배포하는 명령어입니다.
kubectl run --image=nginx --port 80 --replicas=3 nginx-app
아래 명령어를 통해 nginx-app 배포된 노드를 보면 node1 ~ 3 골고루 배포되어 있는걸 확인할 수 있습니다.
kubectl get pod -o wide
정리
이번에 쿠버네티스 고가용성 클러스터를 서버에 구축해보았습니다. 쿠버네티스 버전 1.6 이후 부터는 인증서배포 자동화, 클러스터 연결하는 방법이 간편하게 변경되었습니다. 이전 버전에서는 클러스터 구성하기위해선 복잡한 과정과 설정파일을 만들어야 했지만 이제는 간단하게 명령어 한줄만 입력하면 클러스터에 연결/해지를 하도록 변경되었습니다. 자유로운 노드 할당은 자원 낭비없이 효율적으로 관리하는데 장점이 될 것으로 보여집니다.
참고문서
Get alarms to Telegram in Grafana
1. 알람 채널 추가
- Alerting > Notification channels 메뉴를 선택합니다.

- New channel 을 클릭합니다.

- Type 을 Telegram 으로 선택합니다.


- Name, Default : 모든 알람 받기
- Include image : 이미지 포함 여부
- Disable Resolve Message : 해결 알람 받지 않기
- Send reminders : 알림 상기시키기
- Telegram API settings (BOT API Token, Chat ID) : 텔레그램에 봇을 추가하여 해당 토큰과 사용자의 챗 ID를 기입
설정하고 Save 버튼을 눌러서 저장합니다.
- 추가된 Channel 입니다.

2. 대시보드 패널에 알람 추가
- 초당 요청량과 평균 응답속도 패널에 특정 수치로 알람을 설정 해보겠습니다.

- 추가 하고싶은 패널의 수정 버튼을 누릅니다. 하단의 Alert 설정에서 Create Alert 버튼을 누릅니다.

- 그래프 오른쪽 하트를 위 아래로 드래그하여 알람수치를 조정 할 수 있습니다.

- 알람 이름과 규칙을 정합니다.
Evaluate every 10s For 30s
30초 동안 10초 마다 체크
- Conditions은 여러개를 추가 할 수 있습니다.
WHEN avg () OF query (A, 10s, now) IS ABOVE 400
패널에 추가된 쿼리 A 수치가 10초 전부터 현재까지 평균 값이 400을 넘는 상태

- 평균 응답속도에 대해서도 알람을 설정하겠습니다.
Evaluate every 10s For 0
10초 마다 체크 하도록 설정했습니다.
WHEN max () OF query (B, 10s, now) IS ABOVE 0.03
OR max () OF query (C, 10s, now) IS ABOVE 0.03
쿼리 B, C 중에서 10초 전부터 현재까지 최대값이 0.03초를 넘는 상태에 대해서 알람 설정을 했습니다.

Conditions 에는 다양한 값으로 조건을 설정 할 수 있습니다.

3. 알람 확인 및 활용
- 알림 리스트를 대시보드에 패널로 추가할 수 있습니다.

- 대시보드에 알람 및 리스트가 추가된 모습입니다.

- Telegram 알람 확인 앞서 Notification Channel 에서 Include image 를 설정하였기 때문에 이미지와 함께 알람이 오게 됩니다.
초당 요청량은 ‘Evaluate every 10s For 30s’ 30초 동안 10초 마다 확인 하도록 설정 하였기 때문에 PENDING(보류중) 알람이 오지않고 보류 상태가 됩니다.
평균 응답 속도에는 ‘Evaluate every 10s For 0’ 10초 마다 상태를 확인해서 보내도록 설정하여 보류하지 않고 바로 알람이 오게 됩니다.
- 초록색 : OK
- 주황색 : PENDING
- 빨간색 : ALERTING

- 알람 설정에서 State history 버튼을 눌러서 상태 히스토리를 확인 할 수 있습니다.

- Alerting 메뉴에서 Alert Rules 을 선택하면 알람 설정 수정으로 바로가거나 시작 및 일시중지 제어 할 수 있습니다.

참고 자료
- https://grafana.com/docs/grafana/latest/alerting/
Configuring Monitoring Dashboards Through Grafana
그라파나?
- 그라파나는 데이터를 시각화하여 분석 및 모니터링을 용이하게 해주는 오픈소스 분석 플랫폼입니다. 여러 데이터 소스를 연동하여 사용 할 수 있으며 시각화 된 데이터들을 대시보드로 만들 수 있습니다.
- 앞서 설치한 그라파나와 프로메테우스의 메트릭 정보를 조회하여 시각화 하는 방법에 대해 알아보겠습니다.
구성 방법
1. 데이터 소스 생성
- Create a data source 메뉴를 선택합니다.
- 데이터 소스로 프로메테우스를 선택합니다.
- 프로메테우스 서버 정보를 입력한 후 하단의 Save&Test를 클릭합니다.
- 연결테스트 후 이상이 없다면 저장됩니다.
2. 대시 보드 생성
- 홈으로 돌아와 Build a dashboard 메뉴를 선택합니다.
- 패널 선택장이 나옵니다. 대시보드는 이 패널들을 구성하고 배치하면 됩니다.
- 메트릭 조회를 위해 Add Query를 선택합니다
- Query 선택창에서 앞에 생성한 Data Source를 불러옵니다.
- Metric에선 노드익스포터에서 수집한 항목을 선택합니다.
잠시 돌아와서 위에 선택한 메트릭 항목은 아래와 같은 구조를 가지고 있습니다.
따라서 Metric name으로만 조회시 프로메테우스 서버가 수집한 같은 이름 항목 전부를 조회 합니다. Label name으로 구분하여 조회 할 수 있습니다.
- Prometheus Metric
- 이처럼 프로메테우스 서버에서도 메트릭을 조회할 수 있습니다.
- node_cpu_seconds_total{job=”kube2”,mode=”system”}
- ex) kube2 host에서 system 영역 cpu 사용률
- 다시 돌아와서 생성한 패널 쿼리에 적용해보겠습니다.
- node_cpu_seconds_total는 계속 누적이 되고 있는 값이기 때문에 rate함수를 사용합니다.
- rate(node_cpu_seconds_total{job=”kube2”,mode=”system”}[10m])
- 10분동안 CPU 사용율 변화수치를 초당으로 변환
- CPU 코어의 평균을 구하여 해당 서버의 CPU 평균 사용률을 표시합니다. -avg(rate(node_cpu_seconds_total{job=”kube2”,mode=”system”}[10m])) by (job)
- 설정에서 그래프 종류를 선택할 수 있습니다.
이처럼 자신의 필요한 메트릭 정보를 집계하여 패널로 만든 후 대시보드를 구성하면 됩니다.
외부 대시보드 포맷 사용
- 매트릭 정보를 직접 집계하여 사용하거나 필요한 데이터를 정의하는게 나름 번거로운 작업이라 그라파나 홈페이지에 다른 여러 사용자들이 구성해놓은 대시보드 포맷을 사용하면 더 쉽게 대시보드를구현할 수 있습니다.
https://grafana.com/grafana/dashboards
- import 화면에서 다운받은 JSON 파일을 upload하거나 COPY ID를 입력하여 적용합니다.
결론
프로메테우스, 그라파나를 통해 비교적 쉽게 모니터링을 위한 프로세스들을 만들 수 있었습니다. 여러 익스포터를 통해 필요한 매트릭 수집을 확장할 수 있다는게 큰 장점인 것 같으며 PULL 방식이라 부하에 따른 장애나 성능감소를 걱정하지 않아도 된다고 합니다. 그라나파를 통해 매트릭 데이터를 보기 쉽게 만들 수 있었으며 다양한 데이터 소스를 지원하는 만큼 여러 분야에서 활용하면 좋을 것 같습니다.







