Git을 사용하는 방식은 명령형(커맨드라인; Command line; 또는 터미널 방식) 이거나 GUI Client 형(Mac, Windows)이거나 둘 중 하나일 것이다. Git의 내부 메커니즘을 알기 위해서이기도 하지만 커맨드라인 방식이 익숙해 지면 훨씬 수월해 지는 경우가 많은 듯 하다. 이 때 어쩔 수 없이 git status 를 쳐서 현재 어떤 브랜치에서 작업중인지를 수시로 확인해야 하는데, Linux 의 프람프트를 개조해서 사용하면 편리한 점이 많으므로 그 방법을 정리한다.


"Git 은 브랜치로 시작해서 브랜치로 울고 웃다가 브랜치로 끝난다" - Barracuda -


* 준비물: github 에서 공개된 아래의 스크립트를 계정의 Home에 내려 받아 둔다

* git-prompt.sh 는 Bash, Zsh 를 지원한다

bryan@bryan-XenPC:~$ curl -o ~/.git-prompt.sh https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh


* 홈 디렉토리의 .bashrc를 아래와 같이 수정하고 적용

* 기존의 프람프트에 영향, 변형이 없어야 하며, Git 으로 관리되는 작업 디렉토리에서만 프람프트가 자동으로 변경

bryan@bryan-XenPC:~$ vi .bashrc

...

# For Git prompt # <== Homedir에 .git-prompt.sh 가 있는지 확인하는 if문 추가

if [ -f ~/.git-prompt.sh ]; then 

    source ~/.git-prompt.sh 

fi


if [ "$color_prompt" = yes ]; then

    if [ -f ~/.git-prompt.sh ]; then # <== 원래의 PS1 설정을 위한 if 블록에 __git_ps1 변수 값을 끼워넣는 if 문 추가

        PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$$(__git_ps1 "(Branch:%s)") '

    else

        PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '

    fi

else

    if [ -f ~/.git-prompt.sh ]; then # <== 위와 마찬가지

        PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$$(__git_ps1 "(Branch:%s)") '

    else

        PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '

    fi

fi

unset color_prompt force_color_prompt

...


* 스크립트가 정상 작동하는지 디렉토리를 이동하면서 확인해 본다

~/git-test/gittest 디렉토리와 ~/PycharmProjects/PyTestProjects 는 git 으로 트래킹되는 working 디렉토리이다

bryan@bryan-XenPC:~$ source .bashrc

bryan@bryan-XenPC:~$ cd git-test/gittest/

bryan@bryan-XenPC:~/git-test/gittest$(Branch:develop) cd ../../kernel_src/

bryan@bryan-XenPC:~/kernel_src$ cd ../PycharmProjects/PyTestProjects/

bryan@bryan-XenPC:~/PycharmProjects/PyTestProjects$(Branch:master)


[관련 글]

2015/07/23 - [Technical/Development] - [Git Tip] AWS EC2 VM을 이용한 Git 서버설정과 git 기본 사용법

2015/07/24 - [Technical/Development] - [Git Tip] Git에 대한 궁금증들

2015/08/03 - [Git Tip] Git Branch와 상태를 보여주는 BASH-GIT-PROMPT



- Barracuda -


저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들


Git 을 다루는 엔지니어들이 점점 늘고 있다. 한글 입력상태에서 자판으로 git을 치면 '햣' 이 된다, 햣~! 너도 나도 써야한다라고 하니, 이게 마치 무슨 대세가 된 건 아닌가 착각도 하게 되는데, 막상 써보려니 손에 익고 간편한 cvs, svn 과는 비슷하면서도 뭔가 좀 다르고 어렵기도 하다.


근데, 간편안내서 같은 곳을 보면 "어렵지 않아요 ;)" 하면서 사람을 막 꼬드긴다(가서 보면 더 너무 쉽게 써놔서 더 아리송하다). 그게 대체 뭐길래...하면서 약도 좀 오르고 궁금하기도 하다. 이제, 하나 하나 따져가면서 왜 그런가 고민하고 정리해 두는 버릇이 있는 필자가 git을 한 번 다루어 보려 한다.



Git을 써야 하는 이유


결론부터 간단히 말하자면, Git 은 약간의 개념공부와 실습이 필요한 Tool 이다. cvs, svn 과는 달리 Git은 Distributed+VCS(버전관리도구) 이고 cvs가 하지 못하는 일을 가능하게 하기 때문이다. Linux의 주인공 리누즈 토발츠는 자신이 만든 Git을 소개하면서 cvs를 'devil'(악마...라기 보다는 죄악에 가깝다고 본다) 라고 까지 한다(아래 유튜브 영상 참고). cvs나 svn을 쓰면서 branch-merge하기 전에 풀백업을 받아 두는 엔지니어의 심정이라면 Git을 써야만 하지 않을까?



왜 Git을 써야 하는지에 대해 인터넷에 수많은 문서들이 올라오고 있지만, 이 페이지를 보고 좀 더 명료해 지리라 생각된다.

http://pismute.github.io/whygitisbetter/



아마도, 이 정도 개념만 잡고 개발팀에 투입 돼도 "이 친구 쫌 하는데!" 라는 시선을 받아 볼 수 있을런지도 모르겠다. 칭찬은 엔지니어에게 스팀팩을 놔주는 거 아닐까...



Git 에 관한 필수 용어와 관행(Convention) 또는 약속



master, branch와 origin


* Repository(저장소=줄여서 repo)가 만들어지면(git init) 최초로 유일하게 존재하는 브랜치가 master branch 이고 전체 저장소의 본류가 된다. 이 브랜치는 그 자체로 'master' 라는 이름을 가진다

* 'origin' 은 약속된 이름으로, 현재 push(원격 서버에 업로드) 또는 clone/pull(원격서버로부터 내려받음), Fetch(원격 repo내용 확인) 하려는 원격저장소(서버의 repo)의 기본(default) 이름이다

  - git clone git://remote.blah/test.git 을 하게 되면 자동으로 현재 디렉토리에 test 가 만들어지고 원격저장소의 이름은 'origin' 이 된다(-o 옵션으로 마지막 인수로 새로운 디렉토리명(예: test_dev)을 덧붙여서 다른 이름을 부여할 수도 있다)

  - 굳이 원격저장소의 이름을 origin 으로 하지 않아도 무방하지만, 프로젝트의 최초에 생성되는 원격저장소는 오리지널 이라는 의미의 'origin' 이란 이름을 가지도록 한다

  - origin 이 아닌 원격저장소의 이름은 git remote rename oldname newname , git remote rm name1 ... 이런 식으로 변경 또는 삭제가 가능하다. 또는 다른 저장소이름을 origin 으로 바꾸는 것도 물론 가능하다



작업 디렉토리, 로컬/원격저장소(repository), Index, Head


*작업디렉토리는 버전을 관리할 소스 파일들이 있는 곳이며, git init 으로 초기화한 디렉토리와 그 하위 디렉토리 전체가 관리 대상이다. 이 곳의 전체 또는 일부의 파일들은 git add 로 index라는 가상공간에 등록(staging 단계) 되며, Git에 의한 관리 대상이 된다. 수정 또는 새로이 추가된 파일은 항상 git add 명령으로 스테이징단계를 거쳐야 한다

* Staging 단계의 파일들은 git commit 명령에 의해 repo라는 가상 공간에 기록되며, commit 되는 각 단계의 Head[각주:1] 내용이 기록된 시간 순서대로 꼬리를 물고 늘어서 있는 구조이다. commit 시에 저장되는 내용은 파일 전체 내용이 아닌, 각 파일들의 변경사항들이 묶음으로 기록됨으로써 빠르고 가벼운 동작이 잘 유지된다.

* Git에서 repository는 로컬(development)과 원격에 각각 존재하므로, 개념을 잘 구분해서 써야한다


Git protocol


* Git이 원격저장소와 통신하는 방식은 4가지가 있다

  - 로컬 프로토콜: 로컬 디스크나 NFS로 연결된 저장소일 경우 file:///디렉토리/xxx.git 과 같은 방식으로 연결

  - SSH프로토콜: ssh를 통한 암호화된 통신 방식으로 ssh를 이용할 로그인 계정이 있을 때 사용할 수 있다. 공개용 프로젝트에는 부적합하다

  - Git프로토콜: SSH와 유사하지만 전송 속도가 빠르며 TCP 9418port를 사용한다.  별도의 인증수단이 없어서 Gitosis 와 같은 별도의 인증패키지를 사용해야 한다

  - HTTP/HTTPS 프로토콜: 의미 그대로 HTTP 또는 HTTPS 프로토콜을 그대로 사용하며, 방화벽에 크게 구애받지 않는다


Log 와 Commit Identifier


* Git으로 수행한 모든 commit들은 빠짐 없이 저장소에 기록되며, 각 commit 단계별로 유일한 Key(해시 키) 값(예: f6f9e... 처럼 생긴 16진수들) 이 관리된다.

* git log --graph 와 같은 명령으로 이 Key 값을 조회한 후 revert(되돌리기), cherry-pick(브랜치 가져오기), Tag(꼬리표 달기) 등의 명령을 수행하게 된다


Tag 달기


* Git에서 수행한 commit 단계(의 Key 값)에 대해 의미를 가진 꼬리표를 달아서 버전관리의 이정표를 구분하도록 한다

  - 예: git tag 1.0.0 edbbca65716825e770d1509e9359612260c08c73



Git 과 전문가들이 권고하는 관행(Convention)


reponame.git 형태로 repository 디렉토리명을 쓴다


유일한 오리지널 저장소(repo)인 경우 원격 저장소에 "origin" 이라는 이름을 붙여 쓴다


Branch 의 종류와 네이밍 관행


Vincent Driessen 이 제시하는 브랜칭 모델


backlogtool.com/git-guide 에서 정리한 브랜칭 모델. 위와 모델은 같으나 단순화 되어 눈에 더 잘 들어온다


* Main 브랜치: Master, Develop

* Supporting(Topic) 브랜치: Feature, Release, Hotfix

 - Feature 브랜치: Develop 에서 분기하는 일종의 Topic 브랜치(들), 완료되면 Develop에 병합(merge)한 후 Push

 - Release 브랜치: Develop 에서 릴리즈를 위한 분기(release-브랜치명), 완료되면 Master에 병합한 후 release 태깅

    배포 후 Develop에 병합

 - Hotfix 브랜치: 긴급 수정 작업, Master 에서 긴급 수정작업을 위한 분기(hotfix-브랜치명), 완료되면 Master에 병합, 배포 후 Develop에 병합


[참고]  이 브랜칭 모델이 모든 경우에 정답일 수는 없다. 프로젝트나 사업의 성격상, 어떤 경우는 여러 개의 Release가 고객사별로 브랜칭되어 동시에 병행/교차되어 진행되는 복잡한 구조가 더 적합할 수도 있다. 단, 관리의 복잡성으로 인한 부담이 너무 크지 않다면 말이다. 결국 Git 라는 Tool 자체의 자유도로 인하여, 기존의 SVN등으로 해결 못하던 많은 한계점들을 피해갈 수 있는 가능성이 생겼다는 것은 분명해 보인다.


도움이 되는 온라인 실습/강의/설명 자료


[한글로 된 친절한 문서 페이지들]

https://git-scm.com/book/ko/v2

http://backlogtool.com/git-guide/kr/

http://www.slideshare.net/einsub/svn-git-17386752

http://sapeyes.blog.me/70118257910

https://opentutorials.org/module/217

http://www.creativeworksofknowledge.com/2014/09/03/git-intro-and-common-workflow/



[영문으로 된 실습, 설명, 강의 페이지들]

http://pcottle.github.io/learnGitBranching/ - 한글(일부만) 지원

http://gitref.org/

https://www.atlassian.com/git/tutorials/

http://gitimmersion.com/index.html

http://wildlyinaccurate.com/a-hackers-guide-to-git/


[관련 글]

2015/07/23 - [Git Tip] AWS EC2 VM을 이용한 Git 서버설정과 git 기본 사용법


- Barracuda -

  1. 마치 카세트테이프의 헤드(또는 구식 LP 레코드의 바늘) 처럼, 재생되는 현재의 위치를 가리키는 개념이다. 그래서 merge 옵션에 보면 --no-ff (패스트포워드 하지마삼...) 같은 것도 있다. [본문으로]
저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들


git(깃) 서버를 Amazon EC2 인스턴스에 설치하고, Repo를 운영 관리하는 기초과정 정리


* 준비해야 할 것들

 - 서버: AWS EC2 t2.micro, ubuntu 14.4, 접속주소: ec2-xx.amazonaws.com

 - 클라이언트: Ubuntu 14.4 PC, EC2 vm ssh 접속을 위한 보안 키파일(여기서는 AWSKP_as1.pem)


EC2 vm측, git 서버 설치 과정


* 필수 패키지 설치

root@aws-ubt14-as01:~# apt-get install git-core

root@aws-ubt14-as01:~# apt-get install openssh-server


* linux 계정(=gituser) 추가, 권한 설정 및 key pair 생성

* 이 방법은 git 계정을 공용으로 사용하는 경우로 사용자 개별 설정은 불가(사용자별 권한 설정 등 세부적인 관리가 필요할 경우 gitolite 와 같은 관리 패키지 사용 필요)

root@aws-ubt14-as01:~# adduser --home /home/gituser --shell /bin/bash gituser

root@aws-ubt14-as01:~# visudo <== 아래 line 추가

...

gituser ALL=(ALL) ALL

...

root@aws-ubt14-as01:~# su - gituser


* 생성된 Key pair의 공용(public)키 부분을 .ssh/authorized_keys 에 추가

gituser@aws-ubt14-as01:~ssh-keygen -b 1024 -f gituser -t dsa

gituser@aws-ubt14-as01:~cat gituser.pub >> .ssh/authorized_keys


gituser@aws-ubt14-as01:~chmod 600 .ssh/authorized_keys

gituser@aws-ubt14-as01:~sudo chown -R gituser.gituser .ssh/

gituser@aws-ubt14-as01:~$ mkdir repos; cd repos


* Remote client에서 사용할 작업디렉토리 생성, repo 설정

* 새로운 repo 가 필요해질 때마다 별도로 작업해 주어야 함

gituser@aws-ubt14-as01:~/repos$ mkdir ec2new.git; cd ec2new.git

gituser@aws-ubt14-as01:~/repos/testprj.git$ git init --bare --shared



Remote client(개발자 PC)측 git 설정


* 기존 ec2new 프로젝트를 Local PC에서 git 서버에 push(업로드&동기화)하는 과정까지...

* EC2 vm에서 생성한 key pair의 개인(private)키 파일을 local 로 복사해 와서 저장

* ssh-add 로 키 파일이 자동 적용되게 한 후, EC2 vm 의 gituser 계정에 ssh 로그인이 가능한지 확인

bryan@bryan-XenPC:~$ scp -i AWSKP_as1.pem root@ec2-xxx.amazonaws.com:/home/gituser/gituser ./gituser.pem

bryan@bryan-XenPC:~$ ssh-add /home/bryan/gituser.pem

bryan@bryan-XenPC:~$ ssh gituser@ec2-xxx.amazonaws.com


* 작업 디렉토리(프로젝트: ec2new) 가 있다고 가정

bryan@bryan-XenPC:~/git-test/ec2new$ ls -l

합계 12

-rw-rw-r-- 1 bryan bryan  6  7월 22 20:39 README

-rw-rw-r-- 1 bryan bryan 31  7월 22 20:39 a.c

-rw-rw-r-- 1 bryan bryan 51  7월 22 22:33 b.cpp


* 현재 디렉토리를 git repo 등록(초기화)

bryan@bryan-XenPC:~/git-test/ec2new$ git init


* git repo에 현재 디렉토리의 모든 파일 추가(tracking 가능=staging상태=index에 등록)

bryan@bryan-XenPC:~/git-test/ec2new$ git  add .


* a.c 파일을 수정하였다면 수정 사실을 git에 알린다(관리 대상으로 등록 의미)

bryan@bryan-XenPC:~/git-test/ec2new$ git  add a.c


* Commit 이전까지는 Staging 단계, commit 이되면 HEAD에 기록(= local에 commit)됨

* 즉, git에서 commit 은 local 에서의 최종 저장 단계

bryan@bryan-XenPC:~/git-test/ec2new$ git commit -m "1st commit"

[master 346b586] Modified a.c

 1 file changed, 1 insertion(+)


* Remote repo 주소를 git에게 알림

bryan@bryan-XenPC:~/git-test/ec2new$ git remote add ec2new ssh://gituser@ec2-xxx.amazonaws.com/home/gituser/repos/ec2new.git


* Remote repo 확인

bryan@bryan-XenPC:~/git-test/ec2new$ git remote -v

ec2new ssh://gituser@ec2-xxx.amazonaws.com/home/gituser/repos/ec2new.git (fetch)

ec2new ssh://gituser@ec2-xxx.amazonaws.com/home/gituser/repos/ec2new.git (push)


* Remote repo 에 업로드(최종 기록)

bryan@bryan-XenPC:~/git-test/ec2new$ git push ec2new master

Counting objects: 5, done.

Delta compression using up to 4 threads.

Compressing objects: 100% (2/2), done.

Writing objects: 100% (3/3), 341 bytes | 0 bytes/s, done.

Total 3 (delta 0), reused 0 (delta 0)

To ssh://gituser@ec2-xxx.amazonaws.com/home/gituser/repos/ec2new.git

   eaacfc7..346b586  master -> master

[관련 글]

2015/07/24 - [Technical/Development] - [Git Tip] Git에 대한 궁금증들


- Barracuda -


저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들


MySQL이 Oracle 에 인수될 즈음부터 MySQL을 기반으로 하면서 보다 향상된 개념으로 각자가 이름을 떨치며 꾸준히 진행되어 온 프로젝트가 바로 Percona와 MariaDB이다. 참고로 두 프로젝트의 연관성을 비교하는 내용은 이 곳의 포스팅을 보시면 되겠고, 이번 포스팅에서는 Codership이 만든 Synchronous Multimaster 방식의 Galera cluster 를 설치해 보고, 운영에 관련해서 고려할 점들을 정리해 두려 한다.


Multimaster, Synchronous 한 특징을 가지는 이러한 MM 솔루션이 나오기 전에는(물론 완성도가 떨어지고 운영상 불편했던 MMM 같은 것도 있기는 했다), Master-Slave 구조의 비동기 Replication 방식이 많이 쓰였다. 한 편으로는 Semi-sync(Master는 변경 사항을 Slave 로 전달하는 것 까지만 책임을 진다)라는 장점을 가지기는 했지만 태생적으로 비동기식에서 벗어날 수 없기에, 만약의 상황에서 데이터 손실을 감수해야 하는 한계를 지녔었다고 볼 수 있겠다.


다뤄 나가고자 하는 내용을 간단히 요약하면 다음과 같다


* MariaDB Galera Cluster 설치 및 설정 과정

* 노드의 추가(확장)과 Maintenance를 위한 제거 등 운영 방법



1. MariaDB Galera Cluster 설치 및 설정 과정


설치 방법은 소스 빌드, 바이너리 다운로드&설치, rpm 설치 등 여러 가지가 있지만, 여기서는 mariadb.org 에서 권고하는 distro별 링크를 통해 단계를 밟아 나가는 내용을 그대로 따르면서 진행해 보자.


본 글에서 선택한 설치, 운영 환경: Ubuntu 14.04 Trusty, MariaDB 10, Kaist archive


* 설치 & 테스트를 진행할 3개의 Ubuntu 14.4 머신을 준비한다

 - ubuntu14-pv1, 10.0.10.1

 - ubuntu14-pv2, 10.0.10.2

 - ubuntu14-pv3, 10.0.10.3

* Local network이 아닌 외부 망을 통하여 노드간 원격 접속이 필요한 경우(AWS AZ 도 포함) 또는 서버 자체 방화벽(ufw 등) 이 설정되어 있을 때는, 방화벽 설정에서 TCP 3306/4568/4444 port와 TCP, UDP 4567 을 개방해야 한다(☞참조)



첫 번째 Cluster, Doner 노드의 설치와 기동


* 대다수 작업이 root 권한을 필요로 하므로 super user 로 로그인하여 진행한다

* 설치 과정에서 mysql 관리자 계정인 root 암호를 2번 입력(여기서는 편의상 maria 로 정한다)

* 첫 번째로 설정되어 기동되는 MariaDB 머신 도너(Doner) 노드라고 부르며, 다음과 같이 설정

* 여러 머신간의 데이터 동기화가 중요한 환경에서는, DB 데몬이 머신 부팅 후 자동 실행되는 방식을 피하는 것이 바람직

ubuntu@ubuntu14-pv1:~$ su -

root@ubuntu14-pv1:~# apt-get install software-properties-common

root@ubuntu14-pv1:~# apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db

root@ubuntu14-pv1:~# add-apt-repository 'deb http://ftp.kaist.ac.kr/mariadb/repo/10.0/ubuntu trusty main'

root@ubuntu14-pv1:~# apt-get update

root@ubuntu14-pv1:~# apt-get install mariadb-galera-server

root@ubuntu14-pv1:~# mysql_secure_installation

root@ubuntu14-pv1:~# mysql -uroot -pmaria

MariaDB [(none)]> grant all on *.* to 'root'@'%' identified by 'maria';

MariaDB [(none)]> grant usage on *.* to sst_user@'%' identified by 'maria';

MariaDB [(none)]> grant all privileges on *.* to sst_user@'%';

MariaDB [(none)]> flush privileges;

MariaDB [(none)]> quit

root@ubuntu14-pv1:~# service mysql stop

root@ubuntu14-pv1:~# apt-get install sysv-rc-conf <== mysqld 의 자동 실행을 중지하기 위함

root@ubuntu14-pv1:~# sysv-rc-conf <== runlevel 2~5 에 대해 space 를 눌러서 해제


* 복제 방법으로 Percona 의 xtrabackup 을 사용하기 위함(rsync 를 선택할 경우 설치하지 않아도 됨)

* 이 경우 데이터스트림 전송을 위한 다용도 relay 솔루션인 socat은 꼭 설치해야 함

root@ubuntu14-pv1:~# apt-get install xtrabackup socat


* 중요 설정, 고유 입력 항목은 붉은 글씨로 표시

* wsrep_node_address 에는 머신 자체 ip 를 등록

* 초기 설정시에는 wsrep_cluster_address에 머신 자체 ip 만 등록

* wsrep_sst_receive_address 에는 자체 ip:4569 를 등록

root@ubuntu14-pv1:~# vi /etc/mysql/conf.d/mariadb.cnf

# MariaDB-specific config file.

# Read by /etc/mysql/my.cnf


[client]

default-character-set = utf8

 

[mysqld]

character-set-server = utf8

collation-server = utf8_general_ci

character_set_server = utf8

collation_server = utf8_general_ci

 

autocommit = 0


# Load Galera Cluster

wsrep_provider = /usr/lib/galera/libgalera_smm.so

wsrep_cluster_name='galera_cluster'

wsrep_retry_autocommit = 0

wsrep_sst_auth=sst_user:maria

#wsrep_sst_method = rsync

wsrep_sst_method = xtrabackup

wsrep_provider_options = "evs.keepalive_period = PT3S; evs.suspect_timeout = PT30S; evs.inactive_timeout = PT1M; evs.install_timeout = PT1M"

 

# Galera Node Option

wsrep_node_name='galera1'

wsrep_node_address='10.0.10.1'

wsrep_cluster_address = 'gcomm://10.0.10.1'

wsrep_sst_receive_address=10.0.10.1:4569

 

# Other mysqld options

default-storage-engine=innodb

binlog_format = ROW

innodb_autoinc_lock_mode = 2

innodb_flush_log_at_trx_commit = 2

innodb_locks_unsafe_for_binlog = 1

innodb_log_file_size=100M

innodb_file_per_table

query_cache_size=0

query_cache_type=0

bind-address=0.0.0.0

datadir=/var/lib/mysql

tmpdir=/tmp

user=mysql

log-error=/var/log/mysql/mysql.err


* Cluster 내에서 최초로 기동되는 MariaDB doner 노드이기에 --wsrep-new-cluster 옵션으로 시작

* [주의] Checking for corrupt, not cleanly closed... 메시지는 DB가 정상 기동 되었음을 의미함

root@ubuntu14-pv1:~# service mysql start --wsrep-new_cluster

 * Starting MariaDB database server mysqld                               [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


여기까지가 Doner 노드 설정 과정이다.


* 데이터베이스가 정상 작동하는지 간단히 테스트해 보고, 다음 Cluster 확장 단계로 넘어가자

root@ubuntu14-pv1:~# mysql -uroot -pmaria

MariaDB [(none)]> create database cluster_test;

MariaDB [(none)]> use cluster_test;

Reading table information for completion of table and column names

You can turn off this feature to get a quicker startup with -A


Database changed

MariaDB [cluster_test]> create table tbl1 (id varchar(20));

Query OK, 0 rows affected (0.25 sec)

MariaDB [cluster_test]> insert into tbl1 values ('abcdefg');

Query OK, 1 row affected (0.00 sec)

MariaDB [cluster_test]> commit;

Query OK, 0 rows affected (0.00 sec)



2. 운영 요령: 노드의 추가(확장)과 머신 점검을 위한 제거 등


앞의 단계는 Cluster 에 1개의 머신만 등록한 상태이다. 이번에는 Cluster 내에 2개의 머신을 더 추가하여, Galera Cluster 에서 권장하는 최소 홀수 개인 3개를 완성하는 과정과 Failover 및 자동 복구, Cluster 내에서 노드의 제거(머신 점검 등의 상황일 때)와 재투입 과정에 대해서 정리해 보자.


Cluster 의 두 번째 노드, 첫 Joiner 노드의 추가


* 앞의 Doner 노드 설정 과정과 거의 동일하며, config(maria.cnf) 의 일부 내용과 기동 방법이 다르다


* wsrep_node_address 에는 머신 자체의 ip 를 등록

* wsrep_cluster_address 에 기존의 Doner 노드와 Joiner 노드 ip 를 등록

* wsrep_sst_receive_address 에는 자체 ip:4569 를 등록

root@ubuntu14-pv2:~# scp root@10.0.10.1:/etc/mysql/conf.d/mariadb.cnf /etc/mysql/conf.d/

root@ubuntu14-pv2:~# vi /etc/mysql/conf.d/mariadb.cnf

# MariaDB-specific config file.

...

# Galera Node Option

wsrep_node_name='galera2'

wsrep_node_address='10.0.10.2'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2'

wsrep_sst_receive_address=10.0.10.2:4569

...


* Debian, Ubuntu 계열의 경우 특별히 신경 써서 작업해 주어야 하는 부분(debian-sys-maint 계정을 동일하게)

* Doner 노드의 /etc/mysql/debian.cnf 를 복사(password 만 Doner 노드의 것을 가져와도 됨)

root@ubuntu14-pv2:~# scp root@10.0.10.1:/etc/mysql/debian.cnf /etc/mysql/


* Cluster 내에 추가 되는 Joiner 노드는 별도 옵션 없이 시작

root@ubuntu14-pv1:~# service mysql start

 * Starting MariaDB database server mysqld                                [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


* Doner 노드의 config 를 새로이 추가된 Joiner 를 반영하여 수정해 둔다.

root@ubuntu14-pv1:~# vi /etc/mysql/conf.d/mariadb.cnf

 # MariaDB-specific config file.

...

# Galera Node Option

wsrep_node_name='galera1'

wsrep_node_address='10.0.10.1'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2'

wsrep_sst_receive_address=10.0.10.1:4569

...


* Doner 노드의 Mariadb 를 재시작할 필요는 없다(일단 맞추어 놓기만 하고, 다음에 재시작할 때 읽어 들이면 될 것이다). Joiner 가 추가되면서 이미 내부적으로 서로의 존재가 인식되었기 때문인데, Doner 노드에서 아래의 방법으로 확인할 수 있다. 즉, wsrep_cluster_address 변수는 config 에서 읽어들인 값을 가지고 있지만, Cluster 의 현재 상태 값을 가진 wsrep_incoming_addresses 는 2개 노드 접속 주소를 모두 가지고 있다.

root@ubuntu14-pv1:~# mysql -uroot -pmaria

MariaDB [(none)]> show variables like 'wsrep_cluster_address';

+-----------------------+-------------------+

| Variable_name         | Value             |

+-----------------------+-------------------+

| wsrep_cluster_address | gcomm://10.0.10.1 |

+-----------------------+-------------------+

1 row in set (0.00 sec)


MariaDB [(none)]> show status like 'wsrep_incoming_addresses';

+--------------------------+-------------------------------+

| Variable_name            | Value                         |

+--------------------------+-------------------------------+

| wsrep_incoming_addresses | 10.0.10.2:3306,10.0.10.1:3306 |

+--------------------------+-------------------------------+

1 row in set (0.00 sec)



Cluster 의 세 번째 노드, 새로운 Joiner 노드의 추가


* wsrep_node_address 에는 머신 자체의 ip 를 등록

* wsrep_cluster_address 에 기존의 노드에 추가하여 새로운 Joiner 노드 ip 를 등록

* wsrep_sst_receive_address 에는 자체 ip:4569 를 등록

root@ubuntu14-pv3:~# scp root@10.0.10.2:/etc/mysql/conf.d/mariadb.cnf /etc/mysql/conf.d/

root@ubuntu14-pv3:~# vi /etc/mysql/conf.d/mariadb.cnf

# MariaDB-specific config file.

...

# Galera Node Option

wsrep_node_name='galera3'

wsrep_node_address='10.0.10.3'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3'

wsrep_sst_receive_address=10.0.10.3:4569

...


* Doner 노드의 /etc/mysql/debian.cnf 를 복사

root@ubuntu14-pv3:~# scp root@10.0.10.1:/etc/mysql/debian.cnf /etc/mysql/


* Cluster 내에 추가 되는 Joiner 노드이므로 별도 옵션 없이 시작

root@ubuntu14-pv3:~# service mysql start

 * Starting MariaDB database server mysqld                               [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


* 기존의 Doner 노드와 Joiner 노드의 config 를 새로이 추가된 Joiner 를 반영하여 수정해 둔다. 앞 선 과정과 같은 요령

* 위와 마찬가지로 기존 MaraiDB들을 재시작할 필요는 없다

root@ubuntu14-pv1:~# vi /etc/mysql/conf.d/mariadb.cnf

 # MariaDB-specific config file.

...

# Galera Node Option

wsrep_node_name='galera1'

wsrep_node_address='10.0.10.1'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3'

wsrep_sst_receive_address=10.0.10.1:4569

...

root@ubuntu14-pv2:~# vi /etc/mysql/conf.d/mariadb.cnf

 # MariaDB-specific config file.

...

# Galera Node Option

wsrep_node_name='galera2'

wsrep_node_address='10.0.10.2'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3'

wsrep_sst_receive_address=10.0.10.2:4569

...


여기까지가  두 번째 Joiner 노드 추가 과정이며, 목표로 했던 3대로 이루어진 Galera cluster 가 완성되었다.


* 3대로 구성된 Galera Cluster 가 정상 작동하는지 간단한 테스트를 해 보자

root@ubuntu14-pv1:~# mysql -uroot -pmaria

MariaDB [(none)]> show variables like 'wsrep_cluster_address';

+--------------------------+----------------------------------------------+

| Variable_name            | Value                                        |

+--------------------------+----------------------------------------------+

| wsrep_incoming_addresses | 10.0.10.3:3306,10.0.10.2:3306,10.0.10.1:3306 |

+--------------------------+----------------------------------------------+

1 row in set (0.01 sec)

MariaDB [(none)]> insert into cluster_test.tbl1 values ('xyz123');

1 row in set (0.01 sec)

MariaDB [(none)]> commit;

0 row in set (0.00 sec)


* 최초에 Doner 에서 insert 했던 데이터와 직전에 insert 했던 데이터가 모두 조회된다

root@ubuntu14-pv3:~# mysql -uroot -pmaria -e "select * from cluster_test.tbl1;"

+--------+

| id     |

+--------+

| abcdefg   |

| xyz123   |

+--------+


<여기서 퀴즈1> node1(doner), node2, node3 구성일 때, node1의 MySQL이 shutdown 되었다면 node2, node3 중 하나가 doner 가 될 것이다. 잠시 후 node1이 다시 Cluster에 참여하면 node은 원래대로 doner가 될까?정답은 아래로 Drag!
정답: node1은 원래의 Doner 지위를 자동으로 되찾게 됨


Cluster 내의 노드를 제거/복원(재투입)하려면?


서버 머신 점검을 위해 Doner 노드(ubuntu14-pv1)를 Cluster 에서 제거해야 하는 상황이다. 아래의 과정으로 밟도록 하자.


* config 에서 wsrep_cluster_address 설정을 gcomm:// 로 클리어하고 DB를 재시작하면 Cluster 에서 제거됨

root@ubuntu14-pv1:~# vi /etc/mysql/conf.d/mariadb.cnf

...

# Galera Node Option

wsrep_node_name='galera1'

wsrep_node_address='10.0.10.1'

#wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3' <== Comment out

wsrep_cluster_address = 'gcomm://' <== 추가

wsrep_sst_receive_address=10.0.10.1:4569

...

root@ubuntu14-pv1:~# service mysql restart

 * Stopping MariaDB database server mysqld                               [ OK ] 

 * Starting MariaDB database server mysqld                               [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


root@ubuntu14-pv2:~# mysql -uroot -pmaria -e "show status like  'wsrep_incoming_addresses';"

+--------------------------+-------------------------------+

| Variable_name            | Value                         |

+--------------------------+-------------------------------+

| wsrep_incoming_addresses | 10.0.10.3:3306,10.0.10.2:3306 |

+--------------------------+-------------------------------+

root@ubuntu14-pv3:~# mysql -uroot -pmaria -e "show status like  'wsrep_incoming_addresses';"

+--------------------------+-------------------------------+

| Variable_name            | Value                         |

+--------------------------+-------------------------------+

| wsrep_incoming_addresses | 10.0.10.3:3306,10.0.10.2:3306 |

+--------------------------+-------------------------------+



ubuntu14-pv1 머신의 점검/수리가 끝났다. 원래 대로 재투입하려면 다음 과정을 밟으면 된다.


* config 를 이전 상태로 되돌리고 단순히 restart 하면 끝(이미 Galera Cluster 내에 노드가 1개 이상 작동중일 때에는 --wsrep-new-cluster 옵션을 쓰지 않음)

root@ubuntu14-pv1:~# vi /etc/mysql/conf.d/mariadb.cnf

...

# Galera Node Option

wsrep_node_name='galera1'

wsrep_node_address='10.0.10.1'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3' <== Uncomment

# wsrep_cluster_address = 'gcomm://' <== Comment out

wsrep_sst_receive_address=10.0.10.1:4569

...

root@ubuntu14-pv1:~# service mysql restart

 * Stopping MariaDB database server mysqld                               [ OK ] 

 * Starting MariaDB database server mysqld                               [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


root@ubuntu14-pv2:~# mysql -uroot -pmaria -e "show status like  'wsrep_incoming_addresses';"

+--------------------------+----------------------------------------------+

| Variable_name            | Value                                        |

+--------------------------+----------------------------------------------+

| wsrep_incoming_addresses | 10.0.10.3:3306,10.0.10.2:3306,10.0.10.1:3306 |

+--------------------------+----------------------------------------------+

root@ubuntu14-pv3:~# mysql -uroot -pmaria -e "show status like  'wsrep_incoming_addresses';"

+--------------------------+----------------------------------------------+

| Variable_name            | Value                                        |

+--------------------------+----------------------------------------------+

| wsrep_incoming_addresses | 10.0.10.3:3306,10.0.10.2:3306,10.0.10.1:3306 |

+--------------------------+----------------------------------------------+




3. 노드간 데이터 복제가 잘 되지 않는 것 같다. 확인/조치 방법은?


Cluster 내의 모든 노드들은 자신이 Primary 노드라고 인식한다(참고). 그러나 특정한 상황, 즉 네트워크 일시적 단절(network glitch), 과반수 이상의 노드가 장애를 겪거나 또는 split-brain 상태에 빠질 수도 있다. 이렇게 되면 데이터의 동기화에 문제가 발생할 가능성이 커지게 된다.


[여기서 잠깐] split-brain 에 대해 정리해 둘 필요가 있다. 일반적인 split-brain 이란 Master-Slave 상황에서 각자가 Master(또는 primary) 라고 인식하게 되는 상황을 말한다. Galera Cluster 와 같은 Multimaster 의 경우에도 Doner와 Joiner 관계가 있는 것과 같이 Master라 하더라도 '급' 이 다른 구분이 필요하다(즉, "데이터의 오리지널 소스가 누구지?"에 대한 답이 필요하다). 잠시 후 2-node 구성일 때의 네트워크 단절 상황에 대해 테스트 해보기로 하자.


첫 번째 장애: 3-node 구성일 때 1대의 머신에 네트워크 장애 발생


* node2의 네트워크 단절(머신 자체는 동작하지만 node1, node3 과 네트워킹이 안되도록 iptables 로 장애를 흉내 냄)

* 일정 시간이 지나면 node2 는 '쓰기 불가' 상태에 빠지며 wsrep_local_index가 0으로 떨어짐

root@ubuntu14-pv2:~# iptables -A INPUT -d 10.0.10.2 -s 10.0.10.1 -j REJECT

root@ubuntu14-pv2:~# iptables -A INPUT -d 10.0.10.2 -s 10.0.10.3 -j REJECT

root@ubuntu14-pv2:~# mysql -uroot -pmaria

MariaDB [(none)]> show status like 'wsrep_local_index';

+-------------------+-------+

| Variable_name     | Value |

+-------------------+-------+

| wsrep_local_index | 1     |

+-------------------+-------+


MariaDB [(none)]> show status like 'wsrep_local_index';

+-------------------+-------+

| Variable_name     | Value |

+-------------------+-------+

| wsrep_local_index | 0     |   <== 값이 0인 노드는 Doner 또는 Standalone 노드

+-------------------+-------+


MariaDB [(none)]> show status like 'wsrep_incoming_addresses';

+--------------------------+----------------+

| Variable_name            | Value          |

+--------------------------+----------------+

| wsrep_incoming_addresses | 10.0.10.2:3306 |

+--------------------------+----------------+


MariaDB [(none)]> show status like 'wsrep_cluster_size';

+--------------------+-------+

| Variable_name      | Value |

+--------------------+-------+

| wsrep_cluster_size | 1     |

+--------------------+-------+


MariaDB [(none)]>  show status like 'wsrep_cluster_status';

+----------------------+-------------+

| Variable_name        | Value       |

+----------------------+-------------+

| wsrep_cluster_status | non-Primary |

+----------------------+-------------+


MariaDB [(none)]> insert into cluster_test.tbl1 values ('data1234');

ERROR 1047 (08S01): WSREP has not yet prepared node for application use


[주의사항] Cluster에서 제외된 노드(여기서는 node2)에서는 정상적인 쿼리가 수행되지 않는다

* 이 때 wsrep_provider_options 변수에 pc.bootstrap=1(YES) 값을 설정하면 자체가 Standalone 모드로 작동하게 할 수 있다. 단, 네트워크를 정상화한 이후에 다시 Cluster 내에 투입하려면 반드시 MySQL을 재시작해야 한다

* Cluster 내에서 하나의 노드에서만 수행해야 한다(galera Cluster 에서는 Automatic Bootstrap 이라고 함)

root@ubuntu14-pv2:~# mysql -uroot -pmaria

MariaDB [(none)]> SET GLOBAL wsrep_provider_options='pc.bootstrap=1';


* [주의사항] 또 다른 장애 발생 가능성: 이 상황에서 node2의 특정 테이블에 insert 후 PK 변경 DDL 수행시,  Cluster에 재투입하면 node1, node3 에서는 Deadlock 발생 가능성이 있음

* 따라서, Cluster 에서 제외된 노드에서는 더 이상의 DDL이나 insert/update 쿼리가 돌지 않도록 특별히 유의해야 함

root@ubuntu14-pv1:~# 

MariaDB [cluster_test]> select * from tbl1;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

root@ubuntu14-pv3:~# 

MariaDB [cluster_test]> select * from tbl1;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction


* [원상복귀] node1, node3 은 정상 작동 중인 상황, node2를 되살린다

* node2 의 네트워크를 되살리면 일정 시간이 지나 node1, node2가 있는 Cluster 로 자동 복귀

root@ubuntu14-pv2:~# iptables -F <== 네트워크 장애가 해결되었음을 시뮬레이션



[특정 노드의 전체 데이터 수동 동기화]

* node2 가 Cluster에서 분리된 이후에 Update되어 데이터가 node1, node3 에 비해 지나치게 상이한 경우의 조치(초기화)

* 아래 과정대로 하면 node1, node3로부터 전체 데이터에 대해 동기화(mysqldump, xtrabackup, rsync 등 방법도 있음)

root@ubuntu14-pv2:~# rm -rf /var/lib/mysql/*

root@ubuntu14-pv2:~# rm -rf /var/log/mysql/*

root@ubuntu14-pv2:~# mysql_install_db

root@ubuntu14-pv2:~# service mysql start --wsrep_cluster_address='gcomm://'

root@ubuntu14-pv2:~# mysql_secure_installation

root@ubuntu14-pv2:~# mysql -uroot -pmaria

MariaDB [(none)]> grant all on *.* to 'root'@'%' identified by 'maria';

MariaDB [(none)]> grant usage on *.* to sst_user@'%' identified by 'maria';

MariaDB [(none)]> grant all privileges on *.* to sst_user@'%';

MariaDB [(none)]> flush privileges;

MariaDB [(none)]> quit

root@ubuntu14-pv2:~# service mysql stop

root@ubuntu14-pv2:~# vi /etc/mysql/conf.d/mariadb.cnf

...

# Galera Node Option

wsrep_node_name='galera2'

wsrep_node_address='10.0.10.2'

wsrep_cluster_address = 'gcomm://10.0.10.1,10.0.10.2,10.0.10.3' <== Uncomment

# wsrep_cluster_address = 'gcomm://' <== Comment out

wsrep_sst_receive_address=10.0.10.2:4569

...

root@ubuntu14-pv1:~# service mysql start

 * Stopping MariaDB database server mysqld                               [ OK ] 

 * Starting MariaDB database server mysqld                               [ OK ] 

 * Checking for corrupt, not cleanly closed and upgrade needing tables.


두 번째 장애: 2-node 구성일 때 1대의 머신에 네트워크 장애 발생(split-brain)


* Cluster내에 2개의 노드(node1, node2) 만 남아 있는 상황

* node2 에 네트워크 장애 발생, 2개의 노드에서 동일하게 데이터베이스가 정상작동하지 않음

* Cluster 내의 Master 노드 정족수가 부족(과반수를 초과해야 하나, quorom=1/2)하게 되기 때문(이러한 한계를 무시하는 설정도 있고, 노드를 흉내내 주는 대안적 방법으로 garbd(Galera Arbiter로 2개 노드일 때 quorom 값을 +1 증가시켜 줌, ☞참고) 를 쓰는 방법도 있으나, 썩 바람직하지 않으므로 Skip)

root@ubuntu14-pv2:~# iptables -A INPUT -d 10.0.10.2 -s 10.0.10.1 -j REJECT


root@ubuntu14-pv1:~# mysql -uroot -pmaria

MariaDB [(none)]> select * from cluster_test.tbl1;

ERROR 1047 (08S01): WSREP has not yet prepared node for application use


root@ubuntu14-pv2:~# mysql -uroot -pmaria

MariaDB [(none)]> use cluster_test;

ERROR 1047 (08S01): WSREP has not yet prepared node for application use


* [원상복귀] node2 의 네트워크를 되살리면 일정 시간이 지나 node1이 있는 Cluster 로 자동 복귀

root@ubuntu14-pv2:~# iptables -F


root@ubuntu14-pv1:~# mysql -uroot -pmaria
MariaDB [(none)]> show status like 'wsrep_local_index';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| wsrep_local_index | 0     |
+-------------------+-------+


root@ubuntu14-pv2:~# mysql -uroot -pmaria

MariaDB [(none)]> show status like 'wsrep_local_index';

+-------------------+-------+

| Variable_name     | Value |

+-------------------+-------+

| wsrep_local_index | 1     |

+-------------------+-------+



- Barracuda -


저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들


Redis(레디스; REmote DIctionary System)은 요즘 각광 받고 있는 In-memory Data(key-value) Store이다. 언제고 한 번 다뤄 봤으면 했는데, 마침 비슷한 기회가 주어져서 고가용성을 확보할 수 있는 중요한 설정 방법을 찾아보고 Redis 를 활용하는 아주 간단한 Python Client 예제를 정리하여 실전을 위해 기록해 두고자 한다.


"쓰다 보니 내용이 좀 많습니다. 2편 정도로 나누려 했으나, 다루려고 하는 내용을 한 편에 모아서 구성하는 편이 더 좋다고 생각했으니 스크롤 압박이 심하더라도 양해 바랍니다" - Barracuda -


"Redis 는 DBMS 인가?", "임시 데이터 저장용 캐시라고도 하던데..." 하는 잡다한 얘기는 여기서는 생략하자. 자주 쓰다 보면 아~ Redis 는 이래서 쓰는구나 하고 느끼면 되겠고, 기회가 된다면 왜 Naver Japan에서 LINE 서비스에 이를 적용했는지, Memcached와 왜 자주 비교 되는지, Twitter, Flickr, Github, Tumblr, Instagram 등의 구축 사례를 구글링 등으로 찾아 보면 어느 정도 감이 잡힐 것이라고 생각된다.


본 포스팅에서는 아래의 내용 위주로 정리해 두기로 한다.


* Redis 아키텍처 결정 및 설치/설정 방법

* Redis Sentinel 의 추가 설정과 HA 운영

* Redis Client example - python 버전

* Redis 를 서비스에 적용할 때의 주의사항


1. Redis 아키텍처 결정 및 설치/설정 방법


Redis 로 구성할 수 있는 아키텍처들은 그 용도, 규모에 따른 조합이 다양하게 있을 수 있으며, 응용/확장 형태로 Naver 등에서는 Zookeeper 와 함께 사용하기도 하고, 때로는 Haproxy/Keepalived 를 병행하여 실제 서비스에 사용되기도 한다. 여기서는 Redis 만을 사용한 구성 모델을 생각해 보자.


* Single(단일) 서버 구성 - 개발, 테스트 용도

* MS(Master_1-Slave_N) 구성 - 소규모 개발 및 테스트 용도

* Redis MS & Sentinel*2(Total 5) 구성 - 소규모 개발 및 서비스 용도

* Redis MS & Cluster & Sharding - 중규모 이상 서비스 용도



Redis 와 Sentinel 을 이해하기 위한 간단한 예


여기서는 비교적 단순하면서도 가용성(HA: High Availability)이 보장될 수 있는 3 번째 방법을 채택하고,실제로 구현해 보자. 우선 아래 첫 번째 그림을 보자. Redis 를 설명하는 웹페이지들이 예를 들어 자주 보여 주는 그림이다.


그림 1. Single Maser-Slave & Sentinel


그림1은 하나 씩의 Master-Slave 와, 이들을 감시하는 Sentinel이 Master down시 Slave를 Master 를 승격(Failover 과정을 통해)시켜 주게 되는 좋은 예라고 할 수 있다. 그러나 Redis와 Sentinel 의 작동 과정과 원리를 직관적으로 잘 보여 준다는 점 외에는 이 아키텍처로부터 얻을 수 있는 것은 거의 없다. 왜냐하면 "3대의 서버 중 Master 1대만 죽었을 때(quorom=1일때. quorom은 뒤에 설명)" 만 안정성이 보장된다는 한계를 지닌 아키텍처이기 때문이다.


부하를 분산하기 위해 Master 는 write-only, Slave 는 read-only 로 작동하게 된다고 가정하면, Redis에 접속하여 데이터를 처리하는 Client는 Sentinel로부터 Master와 Slave 의 주소를 알아내려 하기 때문에, Slave나 Sentinel이 죽거나 특정한 2대가 동시에 죽는 어떤 경우에라도 정상적인 서비스를 기대하지 못하게 된다는 사실을 잘 따져 보도록 하자(Sentinel이 왜 필요해 지는지는 아래 실제 구성에서 자연스럽게 정리 될 것이다).



서버 3대로 안정적인 Redis 서비스를 할 수 있는 방법은?


그렇다면, 위와 같은 3대의 서버로 어떻게 하면 비교적 안정적인 서비스 환경을 구성할 수 있을까? 아래 그림을 보자.


그림 2. Redis Master 1, Slave 2 & 3 Sentinels


그림 2는 2대의 서버가 동시에 죽더라도 최소한 Data의 읽기 또는 쓰기 중 한가지만은 가능한 구조이다. Master는 2대의 Slave 들에 각각 변경 데이터를 보내 주며, 3개의 Sentinel 들이 3대의 Redis 서버와 자신을 제외한 나머지 2대의 Sentinel 들을 감시하는 구조이다. Sentinel 을 3대 이상 홀수로 사용하는 이유는, Master 가 죽고 Failover 상황이 발생생했을 때, 홀수개의 Sentinel들이 각각 투표에 참여하여 과반수 이상이 선출되도록 하기 위함이다(redis.io 권고). 그러나 ...


* 이 아키텍처에는 결정적인 한계가 존재한다. 위 그림에서는 Master 서버 내의 Redis 서버(데몬, 서비스)가 Down 되었다면 3개의 Sentinel들이 투표에 참여하여 과반수인 2표(quorom >= 2) 이상을 얻은 Slave 가 Master 로 선출될 것이다. 그런데 뭐가 문제인가?


* 실제 상황에서는 서버(머신) 내의 특정 프로세스 또는 데몬이 죽어 버리는 경우도 있지만, 머신 자체의 하드웨어(전원, 네트워크) 고장/오동작 또는 네트워크 장비/포트 단절 등에 의해 서버 자체의 연결이 끊어지는 경우가 더 많을 수 있다. 그림 1에서와 같은 문제점과 유사하게, Master 서버 머신이 Down 되어 버리거나(왜 문제일까? 아래에서 Quiz 로 제시한다), 2대의 서버가 동시에 Down 되어 버린다면 마찬가지의 참담한 결과를 맞게 된다.


해결 방법은 무엇일까? 단순히 Redis-Sentinel 서버를 1대 더 추가하여 4대를 만들어서 사용하는 방법도 있고, 3대의 Sentinel을 모두 외부의 다른 서버에 설치하거나, Redis 에 접속하는 Client 머신 내에 설치를 권장하기도 한다. 또는 시스템 내의 그리 바쁘지 않은 서버 2대를 따로 선정해서 Sentinel 만 설치/설정해 두면 상대적으로 그림은 간단해지고 위의 문제도 어느 정도 해결 가능하다.



그림 3. Redis Master 1, Slave 2 & 5 Sentinels


그림 3에서는 2대의 서버 머신이 동시에 죽는 어떠한 상황이라도 Redis 서비스 자체는 훌륭하게 동작하게 될 수 있다. 특별히 2대의 Redis 서버가 동시에 다운되었더라도, Redis Client 측에서 Master 를 Write & Read 용으로 사용하도록 약간의 코딩을 해 둔다면 말이다.


* 투입되는 서버의 댓수가 많다면 충분히 안정적인 서비스가 가능하겠지만, 하드웨어나 VM이 무한정 사용될 수는 없는 것이고 보면, 외부 Sentinel 머신을 한 개 정도만 더 추가해서 Redis 3, Sentinel 5 구성이 비교적 합리적인 선택이 아닐까 생각된다(물론 운영의 부담이 되는 관리포인트가 늘어남은 감수해야 한다)


* 결국은 여건을 충분히 고려한 취사 선택이 어느 정도로 필요하다는 말이 되겠고, 본 포스팅에서는 마지막의 그림 3을 목표 아키텍처로 하여 시스템을 준비하고, 실제로 구현하고 검증까지 진행해 보도록 하자.



Redis 서버용 머신을 이용해 Master(1)-Slave(2) 를 구성한다


* 5대의 서버를 준비한다(본 테스트 환경에서 사용한 머신은 모두 ubuntu14.4 vm이며 10.0.0.0/16 네트워크에 연결)

 - ubuntu14-pv1: 10.0.10.1 (Redis Master, Sentinel)

 - ubuntu14-pv2: 10.0.10.2 (Redis Slave1, Sentinel)

 ubuntu14-pv3: 10.0.10.3 (Redis Slave2, Sentinel)

 - ubuntu14-pvha1: 10.0.0.1 (Sentinel)

 - ubuntu14-pvha2: 10.0.0.2 (Sentinel)


그림 4. Redis Master 1, Slave 2


* 1단계 작업으로 그림 4에서와 같이 3대의 서버로 Redis Master-Slave 구성부터 진행

ubuntu14-pv1~3, ubuntu14-pvha1,2 모든 5대의 서버에서 동일하게 작업

root@ubuntu14-pv1:~# apt-get install redis-server redis-tools

root@ubuntu14-pv1:~# apt-get install python-pip

root@ubuntu14-pv1:~# pip install redis

root@ubuntu14-pv1:~# vi /etc/sysctl.conf -> 아래 내용을 추가

# Redis M-S sync

vm.overcommit_memory=1


# Increase max open file limit

fs.file-max = 1048576

root@ubuntu14-pv1:~# sysctl -p

root@ubuntu14-pv1:~# ulimit -n 65535

root@ubuntu14-pv1:~# vi ~/.bashrc -> 스크립트 최 상단에 아래 내용을 삽입

ulimit -n 65535


* ubuntu14-pv1 서버를 Redis master 로 설정하기 위한 작업이다

* 설치 자체는 어렵지 않지만, config 설정시에 주의해야할 것들이 있으니, 붉은 글씨의 라인에 쓰인 설명을 잘 참고한다

* requirepass 는 자신이 Master 가 되었을 때 요구할 접속 암호, masterauth 는 자신이 Slave 일 때 Master 로 접속하기 위한 암호이다. 본 설정에서는 뒤에 Sentinel 에 의해서 Master <-> Slave 전환이 자유로워야 하므로 동일하게 맞추어야 한다.

root@ubuntu14-pv1:~# vi /etc/redis/redis.conf

daemonize yes

pidfile /var/run/redis/redis-server.pid

port 6379

timeout 0

tcp-keepalive 0


bind 10.0.10.1 # 서버 머신 자체의 IP 주소


# slaveof 10.0.10.3 6379 # Master 는 slaveof 설정이 없어야 한다

masterauth 1234

requirepass 1234


repl-ping-slave-period 10

repl-timeout 60


loglevel notice


logfile /var/log/redis/redis-server.log


databases 16


save 900 1

save 300 10

save 60 10000


stop-writes-on-bgsave-error yes


rdbcompression yes

rdbchecksum yes

dbfilename dump.rdb


dir /var/lib/redis


slave-read-only yes


# Yes for high bandwidth, No for high latency or high traffic

repl-disable-tcp-nodelay yes


slave-priority 100


# Mitigation for replication lag, only for Master

#min-slaves-to-write 1

#min-slaves-max-lag 10


appendonly no

appendfilename "appendonly.aof"

appendfsync everysec

no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100

auto-aof-rewrite-min-size 64mb


lua-time-limit 5000


slowlog-log-slower-than 10000

slowlog-max-len 128


notify-keyspace-events ""


hash-max-ziplist-entries 512

hash-max-ziplist-value 64

list-max-ziplist-entries 512

list-max-ziplist-value 64

set-max-intset-entries 512

zset-max-ziplist-entries 128

zset-max-ziplist-value 64


activerehashing yes


client-output-buffer-limit normal 0 0 0

client-output-buffer-limit slave 256mb 64mb 60

client-output-buffer-limit pubsub 32mb 8mb 60


hz 10


aof-rewrite-incremental-fsync yes


* ubuntu14-pv2, 3 서버를 Redis slave 로 설정하기 위한 작업이다

* 붉은 글씨의 라인에 쓰인 설명을 잘 참고한다. 나머지 부분 내용은 Master와 동일하다

root@ubuntu14-pv2:~# vi /etc/redis/redis.conf

daemonize yes

pidfile /var/run/redis/redis-server.pid

port 6379

timeout 0

tcp-keepalive 0


bind 10.0.10.2 # 서버 머신 자체의 IP주소


slaveof 10.0.10.1 6379 # 초기에 Master 로 사용할 1번 서버의 IP주소

masterauth 1234

requirepass 1234


repl-ping-slave-period 10

repl-timeout 60


loglevel debug


logfile /var/log/redis/redis-server.log


databases 16


save 900 1

save 300 10

save 60 10000


stop-writes-on-bgsave-error yes


rdbcompression yes

rdbchecksum yes

dbfilename dump.rdb


dir /var/lib/redis


slave-read-only yes


# Yes for high bandwidth, No for high latency or high traffic

repl-disable-tcp-nodelay yes


slave-priority 100


# Mitigation for replication lag, Comment out for slaves

#min-slaves-to-write 1

#min-slaves-max-lag 10


appendonly no

appendfilename "appendonly.aof"

appendfsync everysec

no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100

auto-aof-rewrite-min-size 64mb


lua-time-limit 5000


slowlog-log-slower-than 10000

slowlog-max-len 128


notify-keyspace-events ""


hash-max-ziplist-entries 512

hash-max-ziplist-value 64

list-max-ziplist-entries 512

list-max-ziplist-value 64

set-max-intset-entries 512

zset-max-ziplist-entries 128

zset-max-ziplist-value 64


activerehashing yes


client-output-buffer-limit normal 0 0 0

client-output-buffer-limit slave 256mb 64mb 60

client-output-buffer-limit pubsub 32mb 8mb 60


hz 10


aof-rewrite-incremental-fsync yes


* Master부터 두 번째 Slave 까지, 모두 위와 같이 작업을 했다면, Master 부터 순차적으로 Redis 를 재시작한다.

* 진행 과정을 관찰하기 위해 redis-server 기동 후 log에 tail 을 걸어 둔다

root@ubuntu14-pv1:~# service redis-server restart

root@ubuntu14-pv1:~# tail -f /var/log/redis/redis-server.log

                _._                                                  

           _.-``__ ''-._                                             

      _.-``    `.  `_.  ''-._           Redis 2.8.4 (00000000/0) 64 bit

  .-`` .-```.  ```\/    _.,_ ''-._                                   

 (    '      ,       .-`  | `,    )     Running in stand alone mode

 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379

 |    `-._   `._    /     _.-'    |     PID: 1140

  `-._    `-._  `-./  _.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |           http://redis.io        

  `-._    `-._`-.__.-'_.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |                                  

  `-._    `-._`-.__.-'_.-'    _.-'                                   

      `-._    `-.__.-'    _.-'                                       

          `-._        _.-'                                           

              `-.__.-'                                               


[1140] 09 Jul 01:13:08.762 # Server started, Redis version 2.8.4

[1140] 09 Jul 01:13:08.763 * DB loaded from disk: 0.001 seconds

[1140] 09 Jul 01:13:08.763 * The server is now ready to accept connections on port 6379

[1140] 09 Jul 01:13:08.763 - DB 0: 633 keys (0 volatile) in 1536 slots HT.

[1140] 09 Jul 01:13:08.763 - 0 clients connected (0 slaves), 834680 bytes in use

[1140] 09 Jul 01:13:09.189 * Starting BGSAVE for SYNC

[1140] 09 Jul 01:13:09.191 * Background saving started by pid 1143

[1143] 09 Jul 01:13:09.218 * DB saved on disk

[1143] 09 Jul 01:13:09.219 * RDB: 0 MB of memory used by copy-on-write



Redis Master에 데이터를 입력하고 Slave 로 복제 되는지 확인하자


* Sentinel 전용 머신(ubuntu14-pvha1,2)에서는 Sentinel만 작동하여야 하므로, redis-server 데몬을 내리고 자동실행을 해제한다(source build 가 아닌  apt-get 같은 방법으로 redis-server 를 설치하면 부팅후 자동실행까지 설정되므로)

root@ubuntu14-pvha2:~# service redis-server stop

root@ubuntu14-pvha2:~# rm /etc/init.d/redis-server

root@ubuntu14-pvha2:~# update-rc.d redis-server remove


* Sentinel 전용 머신(또는 redis-tools 가 설치된 외부 머신)의 redis client 를 이용해서 확인을 수행한다

* Master 에 접속하여 "testkey01":"data1234" Key-Value 를 저장하고 Slave 에서 get 으로 확인

root@ubuntu14-pvha2:~# redis-cli -a 1234 -h 10.0.10.1 -p 6379

10.0.10.1:6379> info replication

# Replication

role:master

connected_slaves:2

slave0:ip=10.0.10.2,port=6379,state=online,offset=1386,lag=0

slave1:ip=10.0.10.3,port=6379,state=online,offset=1386,lag=0

master_repl_offset:1386

repl_backlog_active:1

repl_backlog_size:1048576

repl_backlog_first_byte_offset:2

repl_backlog_histlen:1385

10.0.10.1:6379> set testkey01 'data1234'

OK

10.0.10.1:6379> 

root@ubuntu14-pvha1:~# redis-cli -a 1234 -h 10.0.10.2 -p 6379

10.0.10.2:6379> get testkey01

"data1234"

10.0.10.2:6379> 

root@ubuntu14-pvha1:~# redis-cli -a 1234 -h 10.0.10.3 -p 6379

10.0.10.3:6379> get testkey01

"data1234"


전체적인 동작이 정상적인 것을 확인했다. 자 이 상황에서 Quiz 하나 !(검토하신 아이디어를 공유하거나 질문 하실 분은 댓글로 기재해 주세요)


* <Quiz 1> Master 서버 머신이 Down(redis.io 문서에서는 partition 이라고 표현한다. 즉 shutdown, power down, network 단절 등) 된다면 어떤 일이 일어 나는지 생각해 보자. Master 는 선출될 수 있을까? Read 동작은 가능한가? 원래의 Master 가 다시 연결되면 전체 시스템이 정상 동작 할까?(전제조건: Master 1, Slave 2(read-only), Sentinel 3, quorom=2 일 경우)

[답은 아래로 drag 하세요. 중요한 것은 왜 그럴까를 따져 보는 것입니다]

 - Master 는 선출되지 않는다. 즉, 쓰기 작업은 불가능

 - Read 는 가능하다(Sentinel을 통하여 Slave 를 알아 낸다면)

 - Master 가 살아나면 원 상태로 정상 동작 한다


여기까지 3대의 머신으로 Redis Master-Slave 를 구성하는 기본 작업을 진행했다. Master 를 Write 전용으로, Slave 를 Read 전용으로 하여 부하는 분산하고 데이터를 복제 보존하는 효과까지는 기대할 수 있지만, 자동으로 Failover를 수행하지는 못하는 구성이다.


2. Redis Sentinel 의 추가 설정과 고가용(HA)적 운영


이제 목표 아키텍처인 그림 3 과 같은 구성을 만드는 것이 다음 작업이다.


5대의 머신에 Sentinel을 설정, 기동하고 log 파일을 보면서 진행 과정 관찰


* Sentinel 수행 머신인 ubuntu14-pv1~3, ubuntu14-pvha1,2 에서 각각 다음의 작업을 동일하게 수행한다.

* 모든 Sentinel의 설정 파일은 동일하여야 한다

* 현재의 Redis Master 인 10.0.10.1:6379 를 모니터 대상으로 설정하며, quorom=2, 서비스 이름은 stn-master

root@ubuntu14-pv1:~# vi /etc/redis/sentinel.conf

daemonize yes

pidfile "/var/run/redis/sentinel.pid"

logfile "/var/log/redis/sentinel.log"


# port <sentinel-port>

port 26379


sentinel monitor stn-master 10.0.10.1 6379 2 # <== Failover시 Sentinel 이 자동으로 값들을 수정

sentinel down-after-milliseconds stn-master 2000

sentinel failover-timeout stn-master 90000 <== Failover 완료되어야 하는 시간. 데이터 량에 따라 값을 증가

sentinel auth-pass stn-master 1234

sentinel config-epoch stn-master 1 # <== Failover시 Sentinel 이 자동으로 수정(1씩 증가)


# <=== 여기부터 Sentinel 이 자동으로 설정 내용을 추가, 관리


root@ubuntu14-pv1:~# /usr/bin/redis-sentinel /etc/redis/sentinel.conf

root@ubuntu14-pv1:~# tail -f /var/log/redis/sentinel.log

                _._                                                  

           _.-``__ ''-._                                             

      _.-``    `.  `_.  ''-._           Redis 2.8.4 (00000000/0) 64 bit

  .-`` .-```.  ```\/    _.,_ ''-._                                   

 (    '      ,       .-`  | `,    )     Running in sentinel mode

 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379

 |    `-._   `._    /     _.-'    |     PID: 1155

  `-._    `-._  `-./  _.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |           http://redis.io        

  `-._    `-._`-.__.-'_.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |                                  

  `-._    `-._`-.__.-'_.-'    _.-'                                   

      `-._    `-.__.-'    _.-'                                       

          `-._        _.-'                                           

              `-.__.-'                                               


[1094] 09 Jul 15:58:49.321 # Sentinel runid is 0f83482ecc6a3724d40f4469ac8368a1742be8fd

[1094] 09 Jul 15:58:50.324 * +slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379

[1094] 09 Jul 15:58:50.324 * +slave slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.1 6379

[1094] 09 Jul 15:58:54.013 * +sentinel sentinel 10.0.10.2:26379 10.0.10.2 26379 @ stn-master 10.0.10.1 6379

[1094] 09 Jul 15:58:55.429 * +sentinel sentinel 10.0.10.3:26379 10.0.10.3 26379 @ stn-master 10.0.10.1 6379

[1094] 09 Jul 15:58:56.708 * +sentinel sentinel 10.0.0.1:26379 10.0.0.1 26379 @ stn-master 10.0.10.1 6379

[1094] 09 Jul 15:58:57.791 * +sentinel sentinel 10.0.0.2:26379 10.0.0.2 26379 @ stn-master 10.0.10.1 6379


<...>


root@ubuntu14-pvha2:~# vi /etc/redis/sentinel.conf

root@ubuntu14-pvha2:~# /usr/bin/redis-sentinel /etc/redis/sentinel.conf

                _._                                                  

           _.-``__ ''-._                                             

      _.-``    `.  `_.  ''-._           Redis 2.8.4 (00000000/0) 64 bit

  .-`` .-```.  ```\/    _.,_ ''-._                                   

 (    '      ,       .-`  | `,    )     Running in sentinel mode

 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379

 |    `-._   `._    /     _.-'    |     PID: 26166

  `-._    `-._  `-./  _.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |           http://redis.io        

  `-._    `-._`-.__.-'_.-'    _.-'                                   

 |`-._`-._    `-.__.-'    _.-'_.-'|                                  

 |    `-._`-._        _.-'_.-'    |                                  

  `-._    `-._`-.__.-'_.-'    _.-'                                   

      `-._    `-.__.-'    _.-'                                       

          `-._        _.-'                                           

              `-.__.-'                                               


[4204] 09 Jul 15:58:55.946 # Sentinel runid is 82a6faa318223af0362f59095f8c138be9f9e76d

[4204] 09 Jul 15:58:56.949 * +slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379

[4204] 09 Jul 15:58:56.949 * +slave slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.1 6379

[4204] 09 Jul 15:58:57.617 * +sentinel sentinel 10.0.10.3:26379 10.0.10.3 26379 @ stn-master 10.0.10.1 6379

[4204] 09 Jul 15:58:57.731 * +sentinel sentinel 10.0.10.1:26379 10.0.10.1 26379 @ stn-master 10.0.10.1 6379

[4204] 09 Jul 15:58:57.935 * +sentinel sentinel 10.0.0.1:26379 10.0.0.1 26379 @ stn-master 10.0.10.1 6379

[4204] 09 Jul 15:58:58.182 * +sentinel sentinel 10.0.10.2:26379 10.0.10.2 26379 @ stn-master 10.0.10.1 6379


* 서비스에 투입된 Sentinel 의 정보를 확인해 보자(모든 Sentinel 에서의 조회 값이 동일함)

root@ubuntu14-pvha2:~# redis-cli -a 1234 -h 10.0.10.1 -p 26379

10.0.10.1:26379> info

# Server

redis_version:2.8.4

redis_git_sha1:00000000

redis_git_dirty:0

redis_build_id:a44a05d76f06a5d9

redis_mode:sentinel

os:Linux 3.13.0-55-generic x86_64

arch_bits:64

multiplexing_api:epoll

gcc_version:4.8.2

process_id:1094

run_id:0f83482ecc6a3724d40f4469ac8368a1742be8fd

tcp_port:26379

uptime_in_seconds:177

uptime_in_days:0

hz:11

lru_clock:1036194

config_file:/etc/redis/sentinel.conf


# Sentinel

sentinel_masters:1

sentinel_tilt:0

sentinel_running_scripts:0

sentinel_scripts_queue_length:0

master0:name=stn-master,status=ok,address=10.0.10.1:6379,slaves=2,sentinels=5


이제 설정 과정은 끝이 났다. 테스트 시나리오를 정하고 검증을 수행해 보자. 본 포스팅의 하이라이트라고 할 수도 있는 중요한 부분이다.


Test Case 1: Master 셧다운


* Master에 ssh 로 접속하여 셧다운을 수행하고 Slave 2 의 로그를 살펴 보자

root@ubuntu14-pv1:~# shutdown -h now


* Slave 2(10.0.10.2)의 로그 모니터링, 해설

[1063] 09 Jul 16:03:13.245 # +sdown master stn-master 10.0.10.1 6379 <== Down 감지(주관적)

[1063] 09 Jul 16:03:13.323 # +odown master stn-master 10.0.10.1 6379 #quorum 2/2 <== Down 감지(객관적)

[1063] 09 Jul 16:03:13.323 # +new-epoch 10 <== 후속 조치 발동

[1063] 09 Jul 16:03:13.323 # +try-failover master stn-master 10.0.10.1 6379 <== Failover 발동

[1063] 09 Jul 16:03:13.324 # +vote-for-leader 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 10

[1063] 09 Jul 16:03:13.327 # 10.0.0.1:26379 voted for 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 10

[1063] 09 Jul 16:03:13.327 # 10.0.0.2:26379 voted for 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 10

[1063] 09 Jul 16:03:13.328 # 10.0.10.2:26379 voted for 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 10

[1063] 09 Jul 16:03:13.390 # +elected-leader master stn-master 10.0.10.1 6379 <== 투표 종료

[1063] 09 Jul 16:03:13.391 # +failover-state-select-slave master stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:13.458 # +selected-slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379 <== Slave2(10.0.10.3:6379) 이 최종 선정

[1063] 09 Jul 16:03:13.458 * +failover-state-send-slaveof-noone slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379 <== Slave2 를 10.0.10.1 의 slave 에서 제외

[1063] 09 Jul 16:03:13.513 * +failover-state-wait-promotion slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:14.392 # +promoted-slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.1 6379 <== Slave2 를 Master 로 승격 요청

[1063] 09 Jul 16:03:14.393 # +failover-state-reconf-slaves master stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:14.443 * +slave-reconf-sent slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:14.635 # +sdown sentinel 10.0.10.1:26379 10.0.10.1 26379 @ stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:15.410 * +slave-reconf-inprog slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:15.485 # -odown master stn-master 10.0.10.1 6379

[1063] 09 Jul 16:03:16.423 * +slave-reconf-done slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.1 6379 <== 기존 Slave2의 config 를 변경(10.0.10.2 의 slave로)

[1063] 09 Jul 16:03:16.524 # +failover-end master stn-master 10.0.10.1 6379 <== Failover 종료 알림

[1063] 09 Jul 16:03:16.524 # +switch-master stn-master 10.0.10.1 6379 10.0.10.2 6379

[1063] 09 Jul 16:03:16.525 * +slave slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.2 6379

[1063] 09 Jul 16:03:16.560 * +slave slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.2 6379 <== Down된 10.0.10.1 은 Slave 로 내려감

[1063] 09 Jul 16:03:18.611 # +sdown slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.2 6379


* 이제 1번 과정에서 redis-cli 로 Master에 데이터를 입력(Set)하고 Slave에서 Get 으로 확인하는 과정을 수행해 보자

root@ubuntu14-pvha1:~# redis-cli -a 1234 -h 10.0.10.3 -p 6379

10.0.10.2:6379> keys *

1) "testkey01"

10.0.10.2:6379> set key_into_master2 'data5678'

OK

10.0.10.2:6379> 

root@ubuntu14-pvha1:~# redis-cli -a 1234 -h 10.0.10.2 -p 6379

10.0.10.3:6379> keys *

1) "key_into_master2"

2) "testkey01"

10.0.10.3:6379> 



Test Case 2: 새로운 Master 셧다운


* Case 1 에 이어서 Master(10.0.10.2)에 ssh 로 접속하여 셧다운을 수행하고 Master, Sentinel 머신측 의 로그를 살펴 보자

root@ubuntu14-pv2:~# shutdown -h now


* Slave(10.0.10.2)의 로그 모니터링

[1063] 09 Jul 16:13:22.809 # +sdown master stn-master 10.0.10.2 6379

[1063] 09 Jul 16:13:22.902 # +new-epoch 11

[1063] 09 Jul 16:13:22.902 # +vote-for-leader f7ca052de1d504d263511d5fb9d032c56a0fa351 11

[1063] 09 Jul 16:13:23.910 # +odown master stn-master 10.0.10.2 6379 #quorum 3/2 <== 셧다운 감지

[1063] 09 Jul 16:13:24.666 # +sdown sentinel 10.0.10.2:26379 10.0.10.2 26379 @ stn-master 10.0.10.2 6379

<== Master 완료 후 Failover 완료시까지 시간(sentinel.conf 에 90초 설정) 경과, 결국 Failover 실패로 끝남

[1063] 09 Jul 16:16:22.920 # +new-epoch 12

[1063] 09 Jul 16:16:22.921 # +try-failover master stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:22.921 # +vote-for-leader 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 12

[1063] 09 Jul 16:16:22.923 # 10.0.0.2:26379 voted for 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 12

[1063] 09 Jul 16:16:22.923 # 10.0.0.1:26379 voted for 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 12

[1063] 09 Jul 16:16:22.984 # +elected-leader master stn-master 10.0.10.2 6379 <== 투표 종료

[1063] 09 Jul 16:16:22.985 # +failover-state-select-slave master stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:23.085 # +selected-slave slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.2 6379 <== 유일한 Redis(10.0.10.3:6379) 가 최종 선정

[1063] 09 Jul 16:16:23.086 * +failover-state-send-slaveof-noone slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.2 6379 <== 10.0.10.2 의 slave 에서 제외

[1063] 09 Jul 16:16:23.157 * +failover-state-wait-promotion slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:24.071 # +promoted-slave slave 10.0.10.3:6379 10.0.10.3 6379 @ stn-master 10.0.10.2 6379 <== Master 로 승격 요청

[1063] 09 Jul 16:16:24.072 # +failover-state-reconf-slaves master stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:24.117 * +slave-reconf-sent slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:24.118 # +failover-end master stn-master 10.0.10.2 6379

[1063] 09 Jul 16:16:24.118 # +switch-master stn-master 10.0.10.2 6379 10.0.10.3 6379

[1063] 09 Jul 16:16:24.119 * +slave slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.3 6379

[1063] 09 Jul 16:16:24.146 * +slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379

<== Down 된 10.0.10.1, 10.0.10.2 는 Slave 로...

[1063] 09 Jul 16:16:26.121 # +sdown slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.3 6379

[1063] 09 Jul 16:16:26.212 # +sdown slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379


* Sentinel 전용머신(10.0.0.2)의 로그 모니터링

[4204] 09 Jul 16:13:23.058 # +new-epoch 11

[4204] 09 Jul 16:13:23.058 # +vote-for-leader f7ca052de1d504d263511d5fb9d032c56a0fa351 11

[4204] 09 Jul 16:13:23.700 # +sdown master stn-master 10.0.10.2 6379

[4204] 09 Jul 16:13:23.801 # +odown master stn-master 10.0.10.2 6379 #quorum 3/2

[4204] 09 Jul 16:13:24.841 # +sdown sentinel 10.0.10.2:26379 10.0.10.2 26379 @ stn-master 10.0.10.2 6379

[4204] 09 Jul 16:16:23.079 # +new-epoch 12

[4204] 09 Jul 16:16:23.079 # +vote-for-leader 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3 12

[4204] 09 Jul 16:16:24.496 # +switch-master stn-master 10.0.10.2 6379 10.0.10.3 6379

[4204] 09 Jul 16:16:24.497 * +slave slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.3 6379

[4204] 09 Jul 16:16:24.533 * +slave slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379

[4204] 09 Jul 16:16:26.528 # +sdown slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.3 6379

[4204] 09 Jul 16:16:26.583 # +sdown slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379



2대의 Redis 서버가 남은 상황에서 Master 머신 마저 셧다운 되었을 때의 결과가 흥미롭다. 첫 번째 Failover 는 후보 머신이 더 이상 없어서 Failover timeout(90초)까지 경과하고, 이후의 후속 조치로 마지막 남은 Redis가 Master로 승격되어 처리됨을 확인하였다. 다시 말하면, Redis 시스템 내의 모든 Slave 는 Read-only 이므로, 설정된 Failover timeout 만큼의 시간 동안은 데이터의 쓰기가 실패하게 됨을 잘 알고 상황에 적절히 대처할 수 있어야 한다.



장애 후 복구 과정에서의 처리 절차


위의 Test Case 2까지 진행된 상황에서 redis-cli 로 접속하여 다음과 같이 데이터를 기록해 두자.

root@ubuntu14-pvha1:~# redis-cli -a 1234 -h 10.0.10.3 -p 6379

10.0.10.3:6379> set testkey02 'data7890'

OK


이제 현재의 Master 인 10.0.10.3 의 Redis 에는 최종 버전의 데이터가 저장되어 있다. 여기서 두 번째 Quiz!

셧다운되어 있는 10.0.10.2 머신이 다시 기동되었을 경우 어떤 일이 일어 나는가?10.0.10.2 에 Sentinel 이 기동되었다면 어떤 일이 일어날까? 과연 마지막에 저장한 testkey02 데이터는 보존 될 것인가?


[답은 아래로 drag 하세요] 전제 조건: 기존에 작동하던 3개의 Sentinel 이 작동 상황을 잘 유지하고 있을 경우

10.0.10.2 가 부팅되고  Redis 서비스가 작동 되면, 약간의 시간이 지난 후, 셧다운 되기 전 Master 였던 10.0.10.2는 얌전히 Slave 로 떨어지고, 마지막에 저장된 testkey02 데이터는 이상 없이  보존된다.


* 최종 Master 머신(10.0.0.3)의 로그 모니터링

[1063] 09 Jul 16:16:26.121 # +sdown slave 10.0.10.1:6379 10.0.10.1 6379 @ stn-master 10.0.10.3 6379

[1063] 09 Jul 16:16:26.212 # +sdown slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379

[1063] 09 Jul 16:49:19.799 # -sdown slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379

[1063] 09 Jul 16:50:50.019 * +fix-slave-config slave 10.0.10.2:6379 10.0.10.2 6379 @ stn-master 10.0.10.3 6379 <== Sentinel 에 의해, 셧다운 전에 Master였던 10.0.10.2 Redis는 Slave 로 전환된다



본 포스팅의 실험 결과에서 가장 중요하게 생각해야 할 점은, Sentinel 을 통해 가용성을 유지하기 위한 본 아키텍처에서, Sentinel 을 확인 없이 셧다운한 상태에서 Redis 를 기동하게 되면, 원래 Master 였던 Redis 의 과거 버전 데이터로 인하여 최근 버전에 데이터를 잃어버릴 가능성이 다분하다는 것이다.


그렇다면, 만약 어쩔 수 없는 사정으로 Sentinel 이 작동하지 않게 되어 있고, 수작업으로 최신 데이터를 유지하고 싶다면 어떻게 하면 될까?아래의 순서를 잘 이해하고 차분히 작업을 진행하면 된다.


첫째. 마지막 버전의 데이터를 저장하고 있다고 판단되는 Redis 머신을 Master로 결정한다

둘째, 현재까지 작업에 투입된 모든 Sentinel 을 셧다운하고(# killall redis-sentinel), Redis 서비스도 종료한다(service redis-server stop)

셋째, Redis M-S 구축 첫 단계의 설정에서, Master로 결정한 머신의 redis.conf 에서 slaveof 라인을 comment 처리하고,나머지 머신의 redis.conf 에서 slaveof 라인의 주소를 Master 주소로 변경한다

넷째, 만약을 위해, 각 Redis 머신들의 데이터 파일(/var/lib/redis/dump.rdb)를 백업해 둔다

다섯째, 모든 Sentinel 들의 config 를 아래와 같이 동일하게 수정한다

root@ubuntu14-pvha2:~# vi /etc/redis/sentinel.conf 

...

port 26379


sentinel monitor stn-master 10.0.10.3 6379 2 <== Master로 결정한 머신의 주소로 변경

sentinel down-after-milliseconds stn-master 2000

sentinel failover-timeout stn-master 90000

sentinel auth-pass stn-master 1234

sentinel config-epoch stn-master 1 <== 최종 값을 1로 변경


#

# Generated by CONFIG REWRITE <== 여기부터 마지막 라인까지 삭제

dir "/root"

sentinel known-slave stn-master 10.0.10.1 6379

sentinel known-slave stn-master 10.0.10.2 6379

sentinel known-sentinel stn-master 10.0.10.2 26379 f7ca052de1d504d263511d5fb9d032c56a0fa351

sentinel known-sentinel stn-master 10.0.10.1 26379 0f83482ecc6a3724d40f4469ac8368a1742be8fd

sentinel known-sentinel stn-master 10.0.0.1 26379 9e177cbda8d73c7fc11ac17a6a6d0eef02b3b762


sentinel known-sentinel stn-master 10.0.10.3 26379 1a80939f2d06e6c1d5073ea0dca121c2c02ac5f3

여섯째, 여기 까지의 모든 작업 내용을 다시 점검

일곱째, Redis Master - Slave 순으로 시작, Sentinel 을 모두 시작 후 정상 작동 및 데이터 점검



3. Redis Client example - python 버전


다음은 Redis 와 Sentinel 이 적용된 데이터스토어에 접근하고 각종 Key-Value 데이터를 처리하는 Python Client 의 예제이다. 거의 프로그램 개발 초기의 원형이라고 보면 되겠다. 개발을 위한 라이브러리는 redis-py 를 pip 로 설치하는 정도면 충분하다.


* Redis Master 에 접속하여 쇼핑 데이터를 기록, Slave 에서 데이터를 조회, 출력하는 Python application 이다

( 참고: 1000 명의 고객 쇼핑 카트 정보 기록에 0.18 sec, 읽기에 0.1 sec 소요 - 1cpu, 1GB ram, xen vm)

root@ubuntu14-pvha2:~# apt-get install pip

root@ubuntu14-pvha2:~# pip install redis

root@ubuntu14-pvha2:~/pysrc# vi sentinel_test3.py 

# -*- coding:utf-8 -*-
  
import sys, random, time
from redis import Redis, exceptions, RedisError
from redis.sentinel import (Sentinel, SentinelConnectionPool,ConnectionError,
                            MasterNotFoundError, SlaveNotFoundError)
  
# Redis 접속 기본 설정값
listSentinel = [('10.0.10.1', 26379), ('10.0.10.2', 26379), ('10.0.10.3', 26379), ('10.0.0.2', 26379)]
strServiceName = 'stn-master'
strRedisPass = '1234'
nDB = 0
  
nMaxUser = 1000
  
sentinel = Sentinel(listSentinel, socket_timeout=0.1)
try:
    #sentinel.discover_master(strServiceName) # No need for this
    #sentinel.discover_slaves(strServiceName)
    master = sentinel.master_for(strServiceName, password=strRedisPass, db=nDB, socket_timeout=0.1)
    slave = sentinel.slave_for(strServiceName, password=strRedisPass, db=nDB, socket_timeout=0.1)
  
except MasterNotFoundError:
    print 'Master not found or Sentinel instances not runnung'
    sys.exit()
except SlaveNotFoundError:
    print 'Slave not found or Sentinel instances not runnung'
    sys.exit()
except ConnectionError:
    print 'Connection Error. Check if Sentinel instances are running'
    sys.exit()
  
start_time = time.time()
  
for n in range(1, nMaxUser):
    for m in range(1, random.randint(0, 5)):
        master.hset( "cart.user:"+str(n), random.randint(1, 300), random.randint(1, 5) )
  
time_elapsed_1 = time.time() - start_time
  
start_time = time.time()
  
for n in range(1, nMaxUser):
    slave.hgetall("cart.user:"+str(n))
  
time_elapsed_2 = time.time() - start_time
  
count = 0
  
for n in range(1, nMaxUser):
    data = slave.hgetall("cart.user:"+str(n))
    if len(data) > 0:
        count = count + 1
        print count, " [Cart.user:"+str(n)+"] ", data
  
print "---------------------------------------------------"
print "[Time for writing]: ", time_elapsed_1, " sec., [Time for reading]: ", time_elapsed_2, " sec."



주의 사항: Redis 를 서비스에 적용했을 때의 '대재앙' 을 막아라


운영상 설정 사항 관련 주의 사항

- Sentinel(즉 redis-sentinel) 을 부팅시 자동 실행하도록 두지 마라: 최종 변경 데이터를 날리거나 전체 데이터가 날아갈 수 있다. 귀차니즘은 당신의 몸값을 깎을 수도 있다.

- 데이터를 Disk에 저장하는 save 설정 주기를 짧게 하지 마라: Disk IO 때문에 속도 저하가 발생할 수 있다. 다양한 관찰과 실험을 통해 최적의 save 설정 주기를 찾고 사용하라.

- 데이터 량에 따른 최적의 Failover timeout 값을 찾고 sentinel.conf에 적용하라: 이유는 위에서 설명하였다.

- 메모리 사용 량을 초과하도록 방치하면 안된다: 자칫 메인 메모리를 다 쓰면 가상메모리까지 사용하게 되어 디스크 스와핑이 다량 발생하고, 시스템은 거의 먹통 수준에 빠질 수도 있다.

- Sentinel은 아직은 불완전하다. 결코 과신하지 마라: 덜 Busy한 시간에 주기적으로 dump.db를 백업하고 중요한 장애 복구시는 수동으로 M-S를 지정해서 작업한다.

- Redis.log, Sentinel.log 를 수시로 모니터링하라: Redis 머신의 디스크가 고장나는 등의 하드웨어 장애는 Sentinel이 감지할 수 없다. Master 가 이런 장애를 만나면 Read-only 상태로 바뀔 수도 있다(config 설정에 따라 다르긴 하다)



데이터 관리 방법/설계 관련 주의 사항

- 운영 중에 keys, flushall, flushdb 명령어를 쓰지 마라: 메모리 DB라고 과신하다가 큰 코 다친다. Single thread 방식이며, 데이터 구조와 처리 방식이 상대적으로 Memcached보다 복잡해서 데이터 건수가 많아지면 Memcached 보다는 훨씬 느려진다(널리 알려진 사실이다. 100만건 처리에 멤캐시는 1~2ms, 레디스는 1000ms). 운영 중에 이런 전체 데이터를 긁는 명령을 쓴다면 Redis 를 사용하는 시스템이 일시 먹통이 될 테고, 한 동안 뒤통수가 따가울 수도 있다. 운영지침이나 데이터 관리 등급을 따로 지정해서 체계적으로 데이터를 운영/관리하는 방식을 써야 할지도 모른다

- 개발자는 Redis Master 는 Write-only로, Slave는 Read-only 로 잘 구분해서 잘 써야한다. 코드리뷰시 주요 검토사항이 될 수도 있다.

- 콜렉션(Collection: List, Set, Hash 등) 에는 한 번에 최대 수 천 건 정도의 데이터만 Insert한다.

- 최대 데이터 량을 예측하고, 필요시 클러스터링과 샤딩을 검토하여 아키텍처 설계에 반영하고 개선하라. 때때로 Zookeeper 나 Haproxy 와 같은 다른 솔루션을 검토해야 할 경우도 있을 수 있다.



- Barracuda -


저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들



Haproxy 는 두 말할 것 없이 현존 최고[각주:1]의 오픈소스 소프트웨어 Load Balancing 솔루션이다. 그림에 나오는 구성은 응용 여하에 따라 각종 모바일/웹 서비스의 기본 모델 또는 Hadoop 등 시스템의 하부 솔루션 등에서 Active-Standby 구조의 고가용성 확보 솔루션으로 활용 가능하므로, 본 포스팅을 통해 그 실제적인 부분을 모두 다루어 구현 & 검증/확인해 보고자 한다.


전체적인 구성은 크게 세 부분으로 나뉘는데, 본 구현에서는 앞선 포스팅들("Linux NAT router 설정하기 - Ubuntu 14.4 dom0, xen pv guest 환경", "[Xen 가상화 2] ubuntu pv guest on Ubuntu 14.4 LTS, Xen 4.4.1 - 설치 및 설정 가이드") 에서 다룬 Xen 환경을 확장하여 그대로 써먹고 있으며, nginx 웹서버에서 외부 접속이 가능하도록 설정됨(NAT Router)을 전제로 진행하므로 꼭 참고해 보도록 하자.


1. LB 서버에 Haproxy 설치(LB1: Active, LB2: Stand-by임에 유의)

2. LB 서버에 Keepalived 설치 & VRRP 구동, Failover 확인

3. 내부서버에 웹서버 nginx 설치 & Haproxy 구동 확인 & 테스트

4. (번외)내부서버에서 외부로 나가는 경로를 Keepalived 로 이중화



1. LB 서버에 Haproxy 설치


Ubuntu 에서 Haproxy 를 설치하려면 단순히 apt-get 으로 설치 가능하다. 다만 repo 에 포함된 버전은 1.4.x 버전으로 SSL을 자체 지원하지 않으며, 1.5.x 버전을 설치해야만 SSL 기능을 활용할 수 있다. 소스를 다운로드 받아서 빌드할 수도 있지만 여기서는 ppa:vbernat repo를 배포 받아 설치한다.


* 동일한 과정을 LB1(ubuntu14-pvha1), LB2(ubuntu14-pvha2) 서버에서 각각 수행한다

root@ubuntu14-pvha1:~# apt-add-repository ppa:vbernat/haproxy-1.5

root@ubuntu14-pvha1:~# apt-get update

root@ubuntu14-pvha1:~# apt-get install haproxy

root@ubuntu14-pvha1:~# haproxy -v

HA-Proxy version 1.5.13 2015/06/23

Copyright 2000-2015 Willy Tarreau <willy@haproxy.org>


* haproxy.cfg 파일을 수정한다(LB1, LB2 에서 각각 동일하게 적용)

* 192.168.25.203 은 가상ip(VIP, Virtual IP)이며, 평상시 LB1의 eth0에 binding 되어 있다가 LB1 down시 LB2 eth0가 넘겨 받도록  keepalived 를 통해 설정할 것이다(VRRP by keepalived)

* 서버별 주요 설정 값들은 붉은 글씨로 따로 표시해 둔 점에 유의(주요 설정 수치에 대해서는 haproxy 문서 별도 참조)

root@ubuntu14-pvha1:~# vi /etc/haproxy/haproxy.cfg

global

#log /dev/log local0

#log /dev/log local1 notice

log 127.0.0.1 local2

chroot /var/lib/haproxy

stats socket /run/haproxy/admin.sock mode 660 level admin

stats timeout 30s

user haproxy

group haproxy

daemon


# Default SSL material locations

ca-base /etc/ssl/certs

crt-base /etc/ssl/private


# Default ciphers to use on SSL-enabled listening sockets.

# For more information, see ciphers(1SSL). This list is from:

#  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/

ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS

ssl-default-bind-options no-sslv3


defaults

log global

mode http

option httplog

option dontlognull

option http-server-close

option forwardfor except 127.0.0.0/8

option redispatch

retries 3

timeout http-request 20

timeout queue 86400

timeout connect 86400

timeout client 86400

timeout server 86400

timeout http-keep-alive 30

timeout check 20

maxconn 50000


frontend myLB

bind 192.168.25.203:80

default_backend myLB


backend myLB 192.168.25.203:80

mode http

stats enable

stats hide-version

stats uri /stats

stats realm Haproxy\ Statistics

stats auth haproxy:admin

balance roundrobin

option httpchk GET /hc.html

option http-server-close

option forwardfor

server pv1 10.0.10.1:80 check inter 1000 fastinter 500 downinter 5000 rise 3 fall 1

server pv2 10.0.10.2:80 check inter 1000 fastinter 500 downinter 5000 rise 3 fall 1

server pv3 10.0.10.3:80 check inter 1000 fastinter 500 downinter 5000 rise 3 fall 1

server pv4 10.0.10.4:80 check inter 1000 fastinter 500 downinter 5000 rise 3 fall 1


root@ubuntu14-pvha1:~# vi /etc/rsyslog.conf -> 아래 2개 라인을 찾아 uncomment. UDP syslog 를 port 514 로 받아들이기 위한 config 설정

$ModLoad imudp

$UDPServerRun 514


* haproxy log 가 /var/log/haproxy.log 에 쌓이는지 확인

root@ubuntu14-pvha1:~# service rsyslog restart

root@ubuntu14-pvha1:~# service haproxy restart

root@ubuntu14-pvha1:~# tail -f /var/log/haproxy.log


root@ubuntu14-pvha1:~# vi /etc/default/haproxy -> 아래 라인 추가(부팅시 자동실행)

ENABLED=1



2. LB 서버에 Keepalived 설치 & VRRP 구동, Failover 확인


* 서버 외부의 IP 주소를 NIC에 바이딩이 가능하도록 커널 옵션을 수정한다(LB1, LB2 에서 모두 수행)

root@ubuntu14-pvha1:~# vi /etc/sysctl.conf -> 아래 2개 라인을 입력해 준다

# For keepalived - vrrp

net.ipv4.ip_nonlocal_bind=1

root@ubuntu14-pvha1:~# sysctl -p (또는 리부트)

root@ubuntu14-pvha1:~# cat /proc/sys/net/ipv4/ip_nonlocal_bind 

1


* keepalived 를 설치하고 config 를 설정한다(LB1에서 수행. 우선순위는 1~254 값 중 임의로 사용. 101 값에 유의)

* [중요] Master와 Slave의 Instance 명, virtual router id 를 각각 동일하게 해야 하며, Master 우선순위를 더 높게 하는 이유는, Master 가 내려 갔다가 다시 살아날 때 원래대로 다시 Master 역할을 떠맡아 VIP를 가져가게 하려는 의도

* 기본 eth0에 할당된 192.168.25.201 과 함께 VIP인 192.168.25.203 이 바인딩되도록 설정

root@ubuntu14-pvha1:~# apt-get install keepalived 

root@ubuntu14-pvha1:~# vi /etc/keepalived/keepalived.conf

global_defs {

  router_id ubuntu14-pvha1

}

vrrp_script haproxy {

  script "killall -0 haproxy"

  interval 2

  weight 2

}

vrrp_instance VI_1 {

  virtual_router_id 50

  advert_int 1

  priority 101

  state MASTER

  interface eth0

  virtual_ipaddress {

    192.168.25.203 dev eth0

  }

  track_script {

    haproxy

  }

}

* 최소한의 기본 설정으로, event mail 발송, health-check 튜닝 등 추가적인 부분은 상황에 맞게 별도 진행


* keepalived 를 설치하고 config 를 설정한다(LB2에서 수행. 우선순위 100에 유의)

* 동일한 vrrp instance(VI_1) 가 네트워크 내에 존재하면서 자신보다 우선순위가 높으면 반응하지 않게 된다

root@ubuntu14-pvha1:~# apt-get install keepalived 

root@ubuntu14-pvha1:~# vi /etc/keepalived/keepalived.conf

global_defs {

  router_id ubuntu14-pvha2

}

vrrp_script haproxy {

  script "killall -0 haproxy"

  interval 2

  weight 2

}

vrrp_instance VI_1 {

  virtual_router_id 50

  advert_int 1

  priority 100

  state MASTER

  interface eth0

  virtual_ipaddress {

    192.168.25.203 dev eth0

  }

  track_script {

    haproxy

  }

}


* keepalived, haproxy 순으로 재시작하고 설정 확인(LB1 에서 수행)
root@ubuntu14-pvha1:~# service keepalived restart
root@ubuntu14-pvha1:~# service haproxy restart 
root@ubuntu14-pvha1:~# ip addr show -> eth0에 VIP가 추가로 할당 됨을 확인

...

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether 00:16:3e:fa:5b:07 brd ff:ff:ff:ff:ff:ff

    inet 192.168.25.201/24 brd 192.168.25.255 scope global eth0

       valid_lft forever preferred_lft forever

    inet 192.168.25.203/32 scope global eth0

       valid_lft forever preferred_lft forever

    inet6 fe80::216:3eff:fefa:5b07/64 scope link 

       valid_lft forever preferred_lft forever

...


* keepalived, haproxy 순으로 재시작하고 설정 확인(LB2 에서 수행)
root@ubuntu14-pvha1:~# service keepalived restart
root@ubuntu14-pvha1:~# service haproxy restart 
root@ubuntu14-pvha1:~# ip addr show -> 평상시 eth0에는 VIP가 할당되지 않음을 확인

...

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether 00:16:3e:d9:2c:13 brd ff:ff:ff:ff:ff:ff

    inet 192.168.25.202/24 brd 192.168.25.255 scope global eth0

       valid_lft forever preferred_lft forever

    inet6 fe80::216:3eff:fed9:2c13/64 scope link 

       valid_lft forever preferred_lft forever

...


* Keepalived 의 수행 결과로 VIP가 옮겨 지면서 Haproxy 가 Failover 에 의해 잘 작동되는지 점검한다

1. Host(Dom0)에서 별도 터미널을 띄우고 VIP(192.168.25.203) 쪽으로 ping test를 실행해 둔다

2. LB1, LB2에 각각 1개의 터미널을 띄우고 LB1 서버를 셧다운(halt)한다

3. 위에서 띄운 별도 터미널의 ping test 결과를 관찰하면 약 1~2 회 가량 redirect 발생, 1~2회 가량 순단현상 발생, 이후 정상 ping 결과가 나타난다(이러한 순단현상은 ping test에 의해서 0~2회 정도까지 관찰된다)

bryan@bryan-XenPC:~$ ping 192.168.25.203

...

64 bytes from 192.168.25.203: icmp_seq=221 ttl=64 time=0.291 ms

64 bytes from 192.168.25.203: icmp_seq=222 ttl=64 time=0.297 ms

64 bytes from 192.168.25.203: icmp_seq=223 ttl=64 time=0.242 ms

64 bytes from 192.168.25.203: icmp_seq=224 ttl=64 time=0.286 ms

64 bytes from 192.168.25.203: icmp_seq=225 ttl=64 time=0.151 ms

64 bytes from 192.168.25.203: icmp_seq=226 ttl=64 time=0.270 ms

From 192.168.25.203: icmp_seq=227 Redirect Host(New nexthop: 192.168.25.203)

64 bytes from 192.168.25.203: icmp_seq=229 ttl=64 time=0.337 ms

64 bytes from 192.168.25.203: icmp_seq=21 ttl=64 time=0.288 ms

From 192.168.25.203 icmp_seq=18 Destination Host Unreachable

64 bytes from 192.168.25.203: icmp_seq=230 ttl=64 time=0.328 ms

64 bytes from 192.168.25.203: icmp_seq=231 ttl=64 time=0.316 ms

64 bytes from 192.168.25.203: icmp_seq=232 ttl=64 time=0.319 ms

64 bytes from 192.168.25.203: icmp_seq=233 ttl=64 time=0.318 ms

...
* LB2의 터미널에서 ip addr show 를 수행하면 VIP가 LB1 으로부터 옮겨와서 eth0에 바인딩 된 것을 볼 수 있을 것이다


* LB2 서버의 eth0 에 VIP가 바인딩된 것을 볼 수 있다

root@ubuntu14-pvha2:~# ip addr show

...

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether 00:16:3e:d9:2c:13 brd ff:ff:ff:ff:ff:ff

    inet 192.168.25.202/24 brd 192.168.25.255 scope global eth0

       valid_lft forever preferred_lft forever

    inet 192.168.25.203/32 scope global eth0

       valid_lft forever preferred_lft forever

    inet6 fe80::216:3eff:fed9:2c13/64 scope link

       valid_lft forever preferred_lft forever

...


* 다시 LB1 서버를 켜서, 이전 상태인 LB1 eth0로 VIP가 바인딩 됨은 앞선 테스트 과정을 반복해 보면 확인 가능하다. 이 때에도 약간의 순단현상이 발생할 수 있다.



3. 내부서버에 nginx 설치, haproxy failover 구동 확인


* 4개의 vm(ubuntu14-pv1~ubuntu14-pv4)에 각각 nginx 를 설치하고 테스트를 위한 html source 를 수정

root@ubuntu14-pv1:~# apt-get install nginx

root@ubuntu14-pv1:~# nginx -v

nginx version: nginx/1.4.6 (Ubuntu)

root@ubuntu14-pv1:~# vi /usr/share/nginx/html/index.html -> 기본 제공 html을 간단히 수정

<!DOCTYPE html>

<html>

<head>

<title>Welcome to nginx!</title>

<style>

    body {

        width: 35em;

        margin: 0 auto;

        font-family: Tahoma, Verdana, Arial, sans-serif;

    }

</style>

</head>

<body>

<h1>Welcome to nginx!</h1>

<p>If you see this page, the nginx web server is successfully installed and

working. Further configuration is required.</p>


<p>For online documentation and support please refer to

<a href="http://nginx.org/">nginx.org</a>.<br/>

Commercial support is available at

<a href="http://nginx.com/">nginx.com</a>.</p>


<p><em>Thank you for using nginx.</em></p>

<p>Hostname: ubuntu14-pv1</p>

</body>

</html>


* LB로부터의 Health-check request 을 받기 위한 별도의 html 파일 준비

 root@ubuntu14-pv1:/etc/nginx# vi /usr/share/nginx/html/hc.html 

<!DOCTYPE html>

<html>

<head><title>Health Check file</title></head>

<body>

OK?

</body>

</html>


* (특별 부록1) 외부로 ping 은 나가지만 인터넷 주소로 외부 접근이 안되는 경우 DNS 설정을 해 주어야 한다. Ubuntu 12 부터는 /etc/resolve.conf 만 수정할 경우 리부트 후에 Network manager 에 의해 초기화되므로 다음과 같이 DNS server 를 변경해 주어야 한다.

<고정 ip를 사용할 경우: /etc/network/interfaces, NIC별 직접 설정만으로 충분>

iface eth0 inet static

address 10.0.10.1

...

dns-nameservers 8.8.8.8

<고정 ip 여부에 무관하게 항상 우선적으로 적용되게 할 경우>

root@ubuntu14-pv1:~# vi /etc/resolvconf/resolv.conf.d/head

nameserver 210.220.163.82

nameserver 219.250.36.130

nameserver 8.8.8.8

root@ubuntu14-pv1:~# resolvconf -u


* (특별 부록2) nginx 는 haproxy 와 같은 Load balancer 로부터의 접속에 대해 Client(web browser)의 실제 IP를 손쉽게 로깅할 수 있는 방법을 제공한다(cf: Apache의 경우 mod_rpaf 등의 별도 모듈 설치/설정 필요. 로그파일이나 Web 프로그래밍시에 사용하는  서버변수의 경우에도 Apache는 HTTP_X_FORWARDED_FOR 와 같은 별도의 변수를 써야 하지만, nginx는 LB가 없을 때의 기존 방식 그대로 REMOTE_ADDR 변수를 사용할 수 있다). 이 때 haproxy 측에서는

option http-server-close

option forwardfor

와 같이 config 를 미리 설정하여 X-Forwarded-For 헤더를 웹서버로 전달하도록 하고 nginx 측에는 아래와 같이 설정하면 간단히 구현된다. 여기서 10.0.0.1, 10.0.0.2 는 LB1, LB2(즉 Haproxy 서버) 측의 내부 네트워크 쪽 NIC의 IP 주소들이다.


root@ubuntu14-pv1:~# vi /etc/nginx/nginx.conf -> 붉은 글씨의 라인들 추가

http {

         ...

        # server_name_in_redirect off;


        ##

        # Real IP settings

        ##

        set_real_ip_from 10.0.0.1;

        set_real_ip_from 10.0.0.2;

        real_ip_header X-Forwarded-For;

         ...
         access_log /var/log/nginx/access.log;
         error_log /var/log/nginx/error.log;

         server {
                 listen 80;
                 location ~* /hc.html {
                         access_log off;
                 }
         }

         ##
         # Gzip Settings
         ...
}

root@ubuntu14-pv1:~# service nginx restart

* (특별 부록3) 위의 config 하단을 잘 보면, LB로부터의 지속적인 health-check request 들이 다량으로 로그에 쌓이게 된다. 따라서 config 파일 내의 http 블록 안에 server-location 블록을 설정하여, 80 포트로 들어온 /hc.html 페이지 호출 request 는 access_log 에 쌓지 않도록 설정하였음을 알 수 있다(옵션 사항)



* Haproxy 를 통해 들어온 접속의 Client 측 Real IP 를 확인해 보기 위해 log 에 tail 을 걸어서 확인한다. 웹브라우저는 외부 네트워크인 192.168.25.2에서 Chrome 브라우저를 띄우고 http://192.168.25.203 으로 접속을 시도한다.

root@ubuntu14-pv1:~# tail -f /var/log/nginx/access.log

...

192.168.25.2 - - [01/Jul/2015:11:43:03 +0900] "GET / HTTP/1.1" 200 414 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"

...



* 이후의 테스트 및 검증은 위의 2에서 수행한 것과 유사한 방식으로 LB1, LB2 를 번갈아서 shutdown 하면서 nginx access.log, 테스트용 브라우저의 결과 화면을 모니터링해 보면 되겠다



* 참고로, 일반적으로 많이 사용하는 Apache Bench 를 사용하는 예를 아래에 기재해 두었다

root@bryan-XenPC:~# apt-get install apache2-utils

root@bryan-XenPC:~# ab -n 50 -c 10 http://192.168.25.203/index.html

This is ApacheBench, Version 2.3 <$Revision: 1528965 $>

Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

Licensed to The Apache Software Foundation, http://www.apache.org/


Benchmarking 192.168.25.203 (be patient).....done



Server Software:        nginx/1.4.6

Server Hostname:        192.168.25.203

Server Port:            80


Document Path:          /index.html

Document Length:        642 bytes


Concurrency Level:      10

Time taken for tests:   0.013 seconds

Complete requests:      50

Failed requests:        12

   (Connect: 0, Receive: 0, Length: 12, Exceptions: 0)

Total transferred:      44102 bytes

HTML transferred:       32052 bytes

Requests per second:    3861.60 [#/sec] (mean)

Time per request:       2.590 [ms] (mean)

Time per request:       0.259 [ms] (mean, across all concurrent requests)

Transfer rate:          3326.26 [Kbytes/sec] received


Connection Times (ms)

              min  mean[+/-sd] median   max

Connect:        0    0   0.1      0       1

Processing:     1    2   0.5      2       3

Waiting:        1    2   0.5      2       3

Total:          1    2   0.5      2       4


Percentage of the requests served within a certain time (ms)

  50%      2

  66%      2

  75%      3

  80%      3

  90%      3

  95%      3

  98%      4

  99%      4

 100%      4 (longest request)




4. 내부서버에서 외부로 나가는 경로를 Keepalived 로 이중화하여 Failover


* 웹서비스를 위한 VIP(192.168.25.203) failover에 더하여, 내부서버에서 패키지업데이트 등 유지보수 활동을 위한 외부 접속을 위해 내부서버의 gateway용 VIP(10.0.0.3) 을 keepalived 에 등록한다

* LB1, LB2의 keepalived config를 아래와 같이 수정한다하고 서비스 재시작

root@ubuntu14-pvha1:~# vi /etc/keepalived/keepalived.conf <== 아래 내용을 추가

...

vrrp_instance VI_2 {

  virtual_router_id 100

  advert_int 1

  priority 101

  state MASTER

  interface eth1

  virtual_ipaddress {

    10.0.0.3 dev eth1

  }

  track_script {

    haproxy

  }

}

root@ubuntu14-pvha1:~# service keepalived restart


root@ubuntu14-pvha2:~# vi /etc/keepalived/keepalived.conf <== 아래 내용을 추가

...

vrrp_instance VI_2 {

  virtual_router_id 100

  advert_int 1

  priority 100

  state MASTER

  interface eth1

  virtual_ipaddress {

    10.0.0.3 dev eth1

  }

  track_script {

    haproxy

  }

}

root@ubuntu14-pvha1:~# service keepalived restart


* 4개의 vm(ubuntu14-pv1~ubuntu14-pv4), interfaces config의 gateway 인 10.0.0.1을 VIP인 10.0.0.3으로 수정하고 네트워크 재시작 또는 리부팅(확인 과정은 위의 keepalived 확인 과정과 유사)

root@ubuntu14-pv1:~# vi /etc/network/interfaces

...

auto lo

iface lo inet loopback


# The primary network interface

auto eth0

#iface eth0 inet dhcp

iface eth0 inet static

address 10.0.10.1

netmask 255.255.0.0

network 10.0.0.0

broadcast 10.0.255.255

gateway 10.0.0.3

dns-nameservers 8.8.8.8



[관련글]

2015/06/28 - [Xen 가상화 2] ubuntu pv guest on Ubuntu 14.4 LTS, Xen 4.4.1 - 설치 및 설정 가이드

2015/06/30 - Linux NAT router 설정하기 - Ubuntu 14.4 dom0, xen pv guest 환경

2015/07/01 - Haproxy, Keepalived, nginx 를 이용한 고가용성(High Availablity) 웹서비스 환경 구현



- Barracuda -



  1. 이의 제기가 있어서 Comment 만 달아 둡니다. 잘 알려지고 활용성 높은...정도의 의미입니다. 벤치마킹을 해 보지 않아서 소프트웨어 LB중에 성능, 기능상 최고라고는 얘기 못한다는 말. [본문으로]
저작자 표시 비영리 변경 금지
신고
블로그 이미지

Barracuda

Bryan의 MemoLog. 쉽게 익혀 보는 IT 실습과 개념원리, 코딩 세계의 얕은 맛보기들, 평범한 삶 주변의 현상 그리고 進上, 眞想, 진상들