접속이 늘어서 웹 서버를 한 대 더 놓기로 했다. 놓고 나니 로그인이 자꾸 풀렸다.
Table of contents
Open Table of contents
원인 — 세션이 서버 안에 있었다
설정을 봤다.
// php.ini
session.save_handler = files
session.save_path = "/var/lib/php/session"
session.save_handler 가 files 라 로그인 상태가 그 서버의 파일로 저장된다.
요청 1 → 웹1 → 세션 생성 (웹1 디스크)
요청 2 → 웹2 → 그 세션이 없다 → 로그아웃
두 대에 번갈아 가니 절반은 로그인이 안 된 상태가 된다. 한 대일 때는 문제가 없지만 두 대가 되면 각자 다른 session.save_path 를 들고 있다.
서버를 늘릴 수 있는지가 이 자리로 이미 정해져 있었다. 서버를 늘리는 일이 서버를 늘리는 일로 끝나지 않았다.
세 가지를 놓고 봤다
선택지를 적었다.
1. 같은 사람은 같은 서버로 보낸다
2. 세션 파일을 공유 저장소에 둔다
3. 세션을 DB 나 캐시 서버에 둔다
각각 걸리는 것이 있었다.
1. 한 대가 죽으면 그 서버에 붙어 있던 사람이 다 로그아웃된다
서버마다 부하가 고르지 않다
2. 공유 저장소가 느리면 전체가 느려진다. 잠금 문제가 있다
3. 만들 것이 있다. 저장소가 죽으면 전부 로그아웃된다
쿠키에 담아 요청마다 들고 다니게 하는 방법도 있었다. CodeIgniter 의 세션은 원래 그쪽인데 sess_use_database 가 꺼져 있으면 값 전체를 직렬화해 쿠키에 넣고 서명을 뒤에 붙인다.
3번으로 했다.
session.save_handler = memcached
session.save_path = "cache-01:11211"
넣고 확인했다.
$ curl -c c.txt -s http://web1/login -d 'id=...&pw=...' > /dev/null
$ curl -b c.txt -s http://web2/mypage | grep -o '로그인'
로그인
curl 로 web1 에서 로그인하고 web2 에서 유지되는 것을 확인했다.
저장소가 죽으면 어떻게 되는지 봤다
memcached 를 꺼 봤더니 전원 로그아웃이었다. 한 대가 죽으면 전부 로그아웃되는데 이게 맞는지 따져 봤다.
로그아웃돼도 다시 로그인하면 그만이고 장바구니는 DB 에 있어서 안 없어진다. 다만 결제 중이던 사람은 문제가 된다.
// 결제 시작할 때
$db->insert('payment_session', ['order_no' => $no, 'member_no' => $m, 'data' => $json]);
결제 중인 상태만 DB 에도 남기게 했다.
세션에 무엇을 담는지도 봤다.
$_SESSION['member'] = $memberRow; // 회원 정보 전체
$_SESSION['cart'] = $cartArray; // 장바구니 전체
세션이 컸다. 캐시로 옮기니 매 요청마다 그 크기가 오간다.
$_SESSION['member_no'] = $memberRow['no'];
member_no 만 담고 필요할 때 읽으니 세션 크기가 8KB 에서 200바이트가 됐다.
쿠키에 담는 방식이었으면 더 걸렸을 것이다. 쿠키는 4KB 를 넘으면 브라우저가 조용히 버리는데 setcookie 는 그 크기를 재지 않는다. 넘치면 로그인이 그냥 안 되고 이유가 안 보인다.
자리를 바꾸니 무엇을 담는지도 봐야 했다.
앞단과 여러 대의 일치
앞에 분배기가 생기면서 붙는 사람의 주소가 달라졌다.
$ip = $_SERVER['REMOTE_ADDR'];
전부 분배기 주소로 나와 접속 기록이 의미가 없어졌다.
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
이 헤더는 클라이언트가 직접 붙여 보낼 수도 있어서 앞단에서 온 요청일 때만 믿게 했다. 아무거나 믿으면 차단을 우회당한다.
세션도 여기에 걸릴 수 있었다. CodeIgniter 의 sess_match_ip 가 켜져 있으면 쿠키에 담긴 주소와 지금 주소가 다를 때 세션을 죽인다. 분배기가 여러 대면 그 주소가 요청마다 달라져 로그인이 풀린다.
두 대가 같은 코드인지도 확인이 필요했다.
$ for h in web1 web2; do
echo -n "$h "; ssh $h 'md5sum /app/index.php | cut -c1-8'
done
web1 3f9a2c11
web2 3f9a2c11
같다. 배포가 한쪽만 나가면 절반의 요청이 옛 코드로 처리된다.
$ for i in $(seq 1 20); do curl -s http://shop.example.com/health | grep -o 'web[0-9]'; done | sort | uniq -c
11 web1
9 web2
두 대에 고르게 가는지도 봤다. 한쪽으로 몰리면 늘린 값이 없다. 응답에 서버 이름을 넣어 둔 것이 이때 쓰였는데 없으면 어느 쪽이 처리했는지 알 방법이 없다.
정리
- 로그인 상태를 서버 안에 두면 서버를 못 늘린다
- 세션을 어디에 두었느냐가 배포 형태를 정한다
- 같은 사람을 같은 서버로 보내면 한 대가 죽을 때 그쪽이 다 풀린다
- 공유 자리에 두면 그 자리가 죽었을 때 어떻게 되는지 확인한다
- 없어지면 안 되는 상태는 따로 남긴다
- 자리를 바꾸면 무엇을 담는지도 봐야 한다. 매 요청마다 오간다
- 쿠키는 4KB 를 넘으면 조용히 버려지는데
setcookie는 재지 않는다 - 앞단이 생기면
REMOTE_ADDR이 분배기 주소가 된다 X-Forwarded-For는 앞단에서 온 것만 믿는다- 여러 대가 같은 코드인지 확인한다. 한쪽만 배포되면 절반이 옛 코드다
- 실제로 고르게 나뉘는지도 본다