본문 바로가기

대용량 플랫폼

[박혜웅] 고가용성,확장성을 위한 웹서비스 구조 (Web service architecture for High-Availability and Scalability)

고가용성, 확장성을 위해 소프트웨어로 구성하는 웹서비스 구조에 대해서 얘기해 보려고 한다.

비용에 민감한 중소기업이라면 아래의 구조도 고려해 보자. 대기업이라면 패스~.


먼저 서비스 흐름은 Web Load Balancer(Nginx) -> WAS(Apache+Django) -> Distributed Cache(Redis) -> DB(MySQL) 이다.

각 기능별로 어떻게 이중화하고, 확장 가능한지를 설명해 보면, (DB는 확장하지 않는다 ^^) 


* Web Load Balancer (Nginx)

Nginx는 초경량 웹서버이며, 동시접속자를 수만명이상 처리할 수 있다고 이미 알려져 있다.

WAS 앞단에서 실제로 Client(Browser)의 요청을 처리해 주며, Reverse Proxy로서 정적인 컨텐츠(css, js, html, image)를 빠르게 전송할 수도 있다 또한 Nginx의 여러 기능중에서 Load Balancing 기능을 이용하면 L4장비를 대체할 수 있는데, WAS 장비가 추가될 경우 Nginx에서 IP 하나만 추가하여 Scalability를 보장할 수 있다.


문제점은 Nginx 서버 자체를 어떻게 이중화(redundancy)할지인데, 이는 실제 도메인을 DNS에서 Virtual IP(VIP)에 매핑해 놓고 2대의 Nginx가 서로 VIP를 모니터링하다가 사용하지 않을 경우 스스로 자신의 IP에 추가하는 방법으로 이중화가 가능하다. (물론 DNS Roundrobin 을 이용할 수도 있지만, 이는 DNS의 이중화라는 또 다른 문제점을 만든다.)


아래를 VIP를 이용하여 Fail-Over하는 쉘스크립트다. cron에 의해서 주기적으로 Nginx서버들에 수행하면, 그 중 한대만 VIP를 추가로 할 당받아 Active 상태가 되며, 나머지 Nginx서버들은 Standby 상태로 대기한다.

"서버 인프라를 지탱하는 기술"의 p10~11의 소스를 약간 수정하고, send_arp 대신 arping으로 대체하였다.

#!/bin/sh

# /etc/failover.sh

# apt-get install fake (for send_arp)

# apt-get install arping (for arping)


VIP="1.2.3.4"

DEV="eth0:0"


healthcheck(){

        ping -c 1 -w 1 $VIP > /dev/null

        return $?

}


ip_takeover(){

        MAC=`ip link show eth0 | egrep -o '([0-9a-f]{2}:){5}[0-9a-f]{2}' | head -n 1 | tr -d :`

        ifconfig $DEV $VIP netmask 255.255.255.0 up

#       send_arp $VIP $MAC 255.255.255.255 ffffffffffff

        arping -I eth0:0 $VIP -uc 5

}


################

# by cron

################

DATE=`date "+%Y-%m-%d %H:%M"`

if healthcheck

then

        echo "[$DATE] health ok!"

else

        echo "[$DATE] get ip: $VIP ..."

        ip_takeover

        echo "[$DATE] get ip: $VIP OK."

#        ifconfig

fi


root 계정의 crontab에 아래처럼 등록

* * * * * sudo /etc/failover.sh >> /tmp/failover.log

그리고, 두 서버 모두에서 cron 서비스 시작한다. (master가 될 서버에서 먼저 cron을 시작한다.)

service cron start


테스트할 때는, master에서 아래처럼 eth0:0 인터페이스를 강제로 내려서, slave가 IP를 가져가는지 확인한다.

service cron stop

ifconfig eth0:0 down


참고로 우리 회사에서 사용중인 nginx 가상호스트 설정은 아래와 같은데, domain을  적절한 이름으로 변경해 주면 된다.

(HTTP로 접속했을 경우 강제로 HTTPS로 redirect해주고 SSL 관련 설정이 포함되어 있다. )

# /etc/nginx/sites-available/www.domain.com.vhost


# Upstream WAS servers, e.g.: on port 80

upstream domain {

  server www1.domain.net:80;

  server www2.domain.net:80;

}


proxy_cache_path /var/cache/nginx 

levels=1:2 keys_zone=CACHE1:10m inactive=24h max_size=1g;


# Redirect all HTTP requests to HTTPS.

server {

  listen 80;

  server_name www.domain.net;

  location / {

    rewrite ^/(.*) https://www.domain.net/$1 permanent;

  }

}


# Proxy HTTPS requests on www to WAS servers. (www1, www2, ...)

server {

  listen 443 default_server ssl;

  server_name www.domain.net;


  ssl on;

  ssl_certificate /etc/nginx/ssl/ssl.crt;

  ssl_certificate_key /etc/nginx/ssl/ssl.key;

  ssl_session_cache shared:SSL:1m;

  ssl_session_timeout 5m;

  ssl_client_certificate /etc/nginx/ssl/COMODOHigh-AssuranceSecureServerCA.crt;  


  # Only allow GET, HEAD, and POST requests.

  if ($request_method !~ ^(GET|HEAD|POST)$ ) {

    return 444;

  }


  location / {

    proxy_set_header X-Real-IP $remote_addr;

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_set_header X-Forwarded-Proto https;

    proxy_set_header Host $http_host;

    proxy_next_upstream error;

    proxy_pass http://domain;

    proxy_redirect off;


    proxy_cache CACHE1;

    proxy_cache_valid 200 302 10m;

    proxy_cache_valid 404 1s;

    proxy_cache_use_stale  error timeout invalid_header updating

  http_500 http_502 http_503 http_504;


#    if ($request_uri ~* \.(ico|css|js|gif|jpe?g|png)$) {

#                        expires 72h;

#                        break;

#                }

  }

}


* WAS (Apache+Django)

WAS(Web Applicatioin Server)끼리는 서로 동기화(sync)하고 있다면, 앞단의 Nginx에 의해서 확장성을 보장 받는다. WAS서버가 추가되었을 경우 Nginx의 upstream부분에 새로운 WAS 서버의 IP, Port만 추가해주고 Nginx를 reload하면 끝난다. ^^

다만, WAS서버 자체의 메모리영역에 정보를 저장해서는 안된다. 예를 들어 was1에서 사용자 접속 세션 정보를 자신의 메모리에 올려 놓았을 경우, was2는 이를 알 수 없기 때문이다. 이러한 정보는 뒷단의 Cache에 저장해서 다른 WAS서버들과 공유해야 한다.

또한 사용자가 업로드한 파일을 공유해야 하는데, rsync를 이용해서 짧은 주기로 다른 WAS에 실제로 복사할 수도 있으며, NFS로 공유할 수도 있다. 여유가 된다면 DRDB 또는 분산파일시스템(HDFS등)을 사용하는 것이 좋다.


* Distributed Memory Cache (Redis)

WAS가 DB대신 Cache(Redis)를 read하여, 높은 성능을 보장 받는다. (경험상 DB보다 Distributed Memory Cache가 100~1000배 빠르다) Distributed Cache는 네트웍으로 공유할 수 있는 메모리 캐시를 말한다. 

Server-side에서 구현하는 Redis Cluster는 아직 구현되지 않은 것 같다. (http://redis.io/topics/cluster-spec/

대안으로 Memcached처럼  Client-side에서 Consistent hashing으로 분산시킬 수 있다.

Redis Replication 기능 (http://redis.io/topics/replication)을 통하여, 이중화할 수 도 있다.


* DB (MySQL)

DB는 WAS에서 주로 write만 한다. (read는 cache가 담당.)

MySQL의 Replication기능으로 실시간 동기화 한다. Cache가 있기 때문에 DB는 단지 보관, 통계, 관리자용으로만 사용된다. 따라서 MySQL서버는 master 1대, slave 1대, 총 2대면 충분할 것 같다.

Master DB에서의 작업실수가 Slave에도 그대로 반영되기 때문에, DB자체를 주기적(매시간 또는 매일) 백업하는 것이 필수다. 백업 작업은 Slave에서 처리해야 Master에 부하를 주지 않아 좋다.