제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



대용량 서비스 - 3. 캐쉬

속도 향상에 관련된 여러 가지 방법 중 캐쉬에 대한 이야기를 하려고 합니다. 저는 ehcache를 사용했습니다. ehcache에 관한 문서는 많습니다. 자세한 사용법은 구글형한테 물어보세요^^

1. 캐쉬 서버 구성

캐쉬서버를 별도로 둘 것인지 아니면 개별 컨테이너(tomcat, jboss 등등) 에서 직접 메모리에서 관리할 것인지, 또 서버는 몇 대나 둘 것인지를 고려해야 합니다.

별도의 캐쉬 서버를 두는 것은 비교적 좋은 서버 1대에 캐쉬 정보만 모아서 처리하면 되기 때문에 캐쉬 관리가 비교적 용이합니다. 하지만, 컨테이너 서버와 데어터 통신이 일어나며, 컨테이너 서버에서는 네트워크를 통해 읽어온 데이터를 다시 메모리에 올려서 결과를 처리해야 하기 때문에 캐쉬 성능은 상대적으로 떨어집니다. ( 물론, 그래도 일반적으로 디비 서버에서 데이터를 읽어오는 것보다는 훨씬 좋습니다.)

개별 컨테이너에서 관리하게 될 경우는 보통 메모리에 있는 객체를 바로 사용하기 때문에 네트워크 작업 및 데이터를 사용할 때 마다 메모리에 별도로 올려야 하는 일이 없기 때문에 속도가 빠릅니다. 하지만 문제가 생겼을 때 캐쉬에 관련된 문제인지 프로그램 문제인지 파악하는 게 상대적으로 더 어렵습니다.

캐쉬 서버를 별도로 두건, 개별 컨테이너에서 캐쉬를 관리하건 간에 서버가 여러 대일 경우에는 데이터를 공유하는 방식도 고려해야 합니다. 데이터가 서로 맞아야 하기 때문입니다.

다수의 서버간의 데이터 전파는 전체 전파와 변경 사항만 전파 두 가지로 나뉩니다. 전체 전파는 데이터가 생성, 삭제, 변경 될 때마다 전파하는 방식이고, "변경 사항만 전파" 하는 방식은 데이터의 삭제와 변경 만 전파하는 방식입니다. 변경 사항만 전파하는 경우에는 각각의 서버가 알아서 데이터를 생성하고 삭제나 변경 등이 있을 때만 다른 서버에 알려주는 방식입니다.

관리 편의를 위해서 변경은 따로 두지 않는 것을 권장합니다. 캐쉬란 말 그대로 원천 데이터가 아닌 속도를 빠르게 하기 위해서 쓰는 원천 데이터의 복사본일 뿐입니다. 따라서 캐쉬를 삭제하고 그 캐쉬가 필요한 경우에는 원천 데이터에서 다시 조회해 오면 됩니다. 캐쉬의 변경은 프로그램을 굉장히 복잡하게 하며, 일반적으로 크케 성능을 향상시키는데 도움이 되지도 않습니다. 캐쉬의 내용 변경보다는 그냥 캐쉬 삭제를 하는 게 관리가 훨씬 용이합니다.


2. 캐쉬의 live time

캐쉬의 생존 시간에 대한 문제를 얘기해 봅시다.

1초에 1번씩 요청되는 데이터가 있을 때, 캐쉬의 생존 주기를 10초라고 칩시다. 그러면 대략 캐쉬의 hit rate는 90%가 될 겁니다. (11초 동안 11번의 요청이 있을 것이고, 첫번째 요청에 대해서는 캐쉬에서 데이터를 가져올 수 없을 것이고, 이 후의 10번의 요청은 캐쉬에서 데이터를 가져올 것입니다. 즉 11 번의 요청 중 10번은 캐쉬에서 가져오게 됩니다.)
이 때 live time을 10 배로 늘여봤자, hit rate는 99%가 됩니다. 시간을 10배로 늘여도 캐쉬 효율은 고작 9% 향상될 뿐입니다. (보통 시간을 10배로 늘이면, 메모리 사용량은 거의 10배가 늘어납니다. )

따라서 메모리가 정말 충분하지 않으면, 짧은 시간이라도 캐쉬를 사용하는 게 효율을 극도로 향상 시킬 수 있습니다.
반대로 live time을 마구 늘인다고 캐쉬의 효율이 마구 증가하지도 않습니다. 적절한 메모리 사용량을 체크해서 적절하게 캐쉬의 갯수 및 live time을 조절해야 합니다.


3. 데이터의 출처가 다양할 경우는 data chain을 만들자.

사용자의 GPS 정보를 가져와서 우편번호 정보로 변환해 주는 것을 생각해 봅시다. 구글 같은데 때려 보면 그런 정보들이 나옵니다.

매번 구글에 요청하는 것은 무리입니다.( 구글에 너무 많이 요청하면 막힙니다.) 그래서 구글에서 조회한 데이터는 디비에 저장을 하기로 했습니다.(캐쉬는 일반적으로 메모리입니다. 용량이 넘칠 수 있으니 가능하면 메모리가 아닌 영속적으로 저장할 수 있는 Database와 같은 것이 필요할 겁니다.) 그래서 3가지 방법(캐쉬, 디비, 구글 통신) 으로 데이터를 조회할 수 있습니다.

데이터 조회 정책은 다음과 같습니다.

1. 캐쉬를 뒤진다. - 있으면 데이터 넘기고 끝
2. 없으면 디비를 뒤진다. - 있으면 데이터를 가져와서 캐쉬에 저장하고 끝
3. 없으면 구글에 때린다. - 정보 조회가 성공적이면, 데이터를 디비에 저장하고, 캐쉬에도 저장하고 끝


2번 입장에서는 디비에서 조회를 해왔든 구글 가서 가져왔든 캐쉬에 저장한다는 것은 동일합니다.

이것을 chain of responsibility 패턴을 이용해서 정리하면 깔끔해집니다.

각각의 chain은 다음과 같이 정의 됩니다.


체인 이름 조회 방법 이전 단계 성공시 하는 일
CacheChain캐쉬에서 조회조회된 정보 캐쉬에 저장
DatabaseChainDB에서 조회조회된 정보 디비에 저장
GoogleChain구글에 데이터 요청없음

요청 데이터는 위치 정보입니다. 아래와 같이 정의합니다.( gpsLat, gpsLng 이란 위 경도 값을 가지며 Serializable을 implements 하고 있으며, getter 가 있습니다. Serializable 은 캐쉬에 저장될 때 필요하기 때문에 지정했습니다.)

public class Location implements Serializable {
    private static final long serialVersionUID = 1L;
    private final BigDecimal gpsLat;
    private final BigDecimal gpsLng;
    public Location(BigDecimal gpsLat, BigDecimal gpsLng) {
        this.gpsLat = gpsLat;
        this.gpsLng = gpsLng;
    }
    public BigDecimal getGpsLat() {
        return gpsLat;
    }
    public BigDecimal getGpsLng() {
        return gpsLng;
    }
    //equals , hashCode 잘 구현
}

Chain에서 하는 일을 추상화 시켜보면, 자기 체인에서 데이터를 가져오는 것이전 단계에서 성공시 뭔가를 할 수 있는 것 두 가지가 필요합니다.

따라서 체인은 아래와 같이 정의합니다.(generics 부분은 복잡해 질 수 있으므로 일단 대충...)

public abstract class DataChain<K, V> {
private DataChain<K, V> nextChain;

public DataChain<K, V> setNextChain(DataChain<K, V> nextChain) {
this.nextChain = nextChain;
return nextChain;
}

public final V get(K key) {
V v = getChainData(key);
// 조회에 실패했고 다음 체인이 있으면..
if (v == null && nextChain != null) {
v = nextChain.get(key);
// 다음 체인에서 조회 성공시 할 일을 한다.
if (v != null) {
onSuccess(v);
}
}
return v;
}

protected abstract V getChainData(K key);

protected void onSuccess(V value) {
}
}

외부로 공개되는 public method 2개 protected method 2개입니다.
setNextChain 은 일반적인 chain of resposiblity 에 등장하는 setNext 입니다.
그리고 get 이 데이터를 가져오는 메쏘드입니다.

protected로 정의된 것은 2개가 있는데 getChainData 는 자기 체인에서 데이터를 가져오는 방법을 정의하면 됩니다.
onSuccess는 다음 체인에서 데이터를 성공적으로 가져왔을 때 처리하는 로직을 넣으면 됩니다.


대략 코드는 아래와 같이 됩니다.

-- 캐쉬용 체인
public class CacheChain extends DataChain<Location, String> {
protected String getChainData(Location key) {
// 캐쉬에서 데이터 뽑아서 리턴
}

protected void onSuccess(String value) {
// 캐쉬에 데이터 저장
}
}

-- 디비용 체인
public class DataBaseChain extends DataChain<Location, String> {
protected String getChainData(Location key) {
// 디비에서 데이터 뽑아서 리턴
}

protected void onSuccess(String value) {
// 디비에 데이터 저장
}
}

-- 구글용 체인
public class GoogleChain extends DataChain<Location, String> {
protected String getChainData(Location key) {
// 구글에서 데이터 뽑아서 리턴
}
// 얘는 성공했을 때 자기 체인에서 할 일이 없다.
}

위와 같이 만들고 

DataChain<Location, String> dc = new CacheChain();
dc.setNext(new DataBaseChain()).setNext(new GoogleChain());

과 같이 한 번만 만들어 놓고

String zipcode = dc.get(new Location(new BigDecimal(127.312), new  BigDecimal(37.432));

와 같이 호출하면 우편번호를 뽑아낼 수 있습니다. Chain of responsiblity 가 늘 그렇듯, 클라이언트는 어느 체인에서 처리했는지 관심 없습니다.

게다가 구글에 막히면 daum에 때리는 Chain을 추가한다거나 하는 식의 프로그램 변경이 매우 용이합니다. 또 onFail 같은 것을 만들면, default value 를 chain을 이용해서 처리할 수도 있습니다. 아니면 이 디비 저 디비에 데이터가 산재해 있을 경우 (메모리 디비도 있고, 디스크 디비도 있을 때나 master, slave 구조의 replication 등을 쓸 때) 조회하는 순서를 chain의 순서로 정의하면 됩니다.


4. 최대한 캐쉬를 써야하는 것들

무거운 디비 쿼리
count 와 같이 부하를 많이 줄 수 있는 디비 쿼리는 캐쉬를 써서 유지하면 쿼리 갯수를 획기적으로 줄일 수 있습니다.

통신
위에서 설명한 것처럼 구글 같은데 요청을 날리는 것은 비용이 많이 듭니다. 이런 것도 반드시 캐쉬를 써서 성능을 높이는 게 좋습니다.

거의 변경되지 않는 데이터
우편번호 주소 시스템과 같은 경우는 어징간해서는 변경되지 않습니다. 이런 것들은 디비 조회보다 캐쉬에 영속적으로 박아놓고 쓰는 게 훨씬 빠릅니다.
게시판 데이터도 상대적으로 수정이 많은 것 같지만, 쓰기와 읽기 비율이 1 : 10 정도만 되어도 캐쉬를 쓰는 게 훨씬 유리합니다. 총 11번의 요청(1번 쓰기 + 10번 읽기 ) 중 디비에 접속하는 것은 2번 (1번 쓰기 + 1번 읽기) 밖에 안 되기 때문에 산술적으로 디비 접속 횟수가 확 줄어듭니다.(물론, 쓰기와 읽기의 비용이 다르고 그런 것들은 있지만 어쨌거나 읽기 접속 9번은 하지 않아도 되니까요.)


by 삼실청년 | 2011/11/25 13:36 | 컴터질~ | 트랙백 | 핑백(2) | 덧글(6)

트랙백 주소 : http://iilii.egloos.com/tb/5576319
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at happy2v : [Link].. at 2012/02/17 21:23

... 삼실 청년 블로그 대용량 서비스 만들기 - 1. 적당한 데이터 클래스 대용량 서비스 만들기 - 2. Thread safe vs none Thread safe 대용량 서비스 - 3. 캐쉬 ... more

Linked at 건실성실착실 3실 청년! : .. at 2012/10/06 00:04

... aManager로 정의를 하고 DataManager 안에서는 chain of responsibility 를 이용해서 캐쉬를 잘 정리하는 게 좋습니다. 자세한 건 요기 를 참조하세요. 프로그램쪽에서만 보면.. UserData user = UserByName.get("홍길동");int maxId = MaxMsgIdByUs ... more

Commented by Ceik at 2011/11/25 15:02
좋은 글 정말 잘 보고 갑니다..
보고 많은 걸 느꼈습니다 ^^
Commented by 삼실청년 at 2011/11/28 21:20
캐쉬를 직접 써서 쓰기 전과 시간 비교를 해보면 정말 개선된 느낌이 확 들겁니다.
나중에 실행 속도 측정 등에 대해서도 올릴 예정인데 시간되시면 디비 쿼리와 캐쉬를 비교해 보세요.
캐쉬를 쓰게 되면 안 쓰는 것보다는 프로그램이 복잡해 질 수 있는데,(당연한 얘기지만..-_-;) chain of responsibility 를 쓰게 되면 그나마 복잡도 그다지 올라가지 않더군요.
Commented by 용식 at 2011/11/28 17:46
좋은 내용 잘 보고 갑니다. ^^
머리속에서 잊혀져 가던 패턴 하나가 나와서
잘 적용되는 샘플을 보고 많이 배워갑니다. :)
Commented by 삼실청년 at 2011/11/28 21:28
컴터라는 게 만병 통치약이라는 게 없고 딱 그 케이스에만 훌륭한 답을 보이는 게 대부분이라 그런 케이스와 종종 대면하지 않으면 해결법 자체를 좀 까먹게 되는 경향이 있는 것 같아요.
결론은 닥치고 공부..
Commented by 진우 at 2012/12/06 02:36
정말 공부할께 많은걸 또 느끼네요.
잘 지내세요?
Commented by 삼실청년 at 2012/12/07 00:19
응 머 걍 살고 있어. 넌 이제 외국인된거냐? 안 오냐?

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶