티스토리 뷰

발생일: 2009.04.16

문제:
발송한 메일을 몇 명이나 개봉했는지 알기 위해,
메일 내에 사이즈가 0인 이미지 태그를 두어 개봉 횟수를 업데이트하는 서블릿을 호출하도록 했다.

해당 서블릿에서는 메일 아이디를 패러미터로 받아,
매 요청마다 아이디에 해당하는 데이터를 찾아 디비에서 카운트를 1씩 업데이트 해주고 있었다.

문제는 아침 뉴스 메일을 발송하면서부터 발생했다.
많은 사용자가 동시에 메일을 개봉하면서 해당 서블릿에 갑자기 많은 요청이 들어오게 된 것이다.

매 요청마다 디비에 접속해서 업데이트 하다보니 동시 요청에 대해 조금씩 지연처리되기 시작했고,
급기야는 디비 쓰레드풀이 꽉 차서 시스템 자체가 멎어버리는 현상이 발생했다.

해결책:
개봉 횟수를 업데이트하는 서블릿에 요청이 들어왔을 때에 바로 디비에 업데이트하는 대신,
해당 아이디 값과 누적된 카운트를 context attribute 에 저장하는 방법을 사용했다.

저장된 값은 어플리케이션의 백그라운드에서 작동하는 데몬 스레드을 생성해
정해진 시간마다 컨텍스트 속성에 담긴 값을 가져와 디비에 업데이트했다.

중복된 요청을 정상적으로 처리하기 위해 동기화에 대한 처리가 필요했으며 아래는 샘플 메서드다.
컨텍스트 속성으로부터 저장된 mapName에 해당하는 HashMap을 가져오는 메서드를 만들었다.

    public HashMap getMapFromContextAttribute(String mapName, ServletContext sc) {
        if (sc.getAttribute(mapName) == null) {
            synchronized (sc) {
                if (sc.getAttribute(mapName) == null) {
                    sc.setAttribute(mapName, new HashMap());
                }
            }
        }
        return (HashMap) sc.getAttribute(mapName);   
    }


위에서 가져온 map을 패러미터로 하여,
name에 해당하는 값을 찾아 count를 더해주는 역할을 하는 메서드다.

    public void addCountToMap(String name, HashMap map) {
        if (map.get(name) == null) {
            synchronized(map) {
                if (map.get(name) == null) {
                    map.put(name, new Integer(0));
                }
            }
        }
       
        synchronized (map) {
            int cnt = ((Integer) map.get(name)).intValue();
            map.put(name, new Integer(cnt + 1));
        }
    }


백그라운드에서 작업하는 데몬 스레드에서는 요청에 따라 담겨진 map을 가져와 디비에 업데이트 한다.
여기서도 동기화가 필요한데, context에 오랫동안 lock을 걸고 있으면 부담이 크기 때문에
context에 저장된 map을 복사해와 디비에 업데이트 한다.
아래는 컨텍스트에서 map을 복사해오기 위해 만든 메서드이다.

    public HashMap copyMapFromContext(String name, ServletContext context) {
        HashMap copyedMap = new HashMap();
       
        synchronized (context) {
            HashMap contextMap = (HashMap) context.getAttribute(name);
            if (contextMap != null) {
                for (Iterator it = contextMap .entrySet().iterator(); it.hasNext();) {
                    Entry entry = (Entry) it.next();
                    copyedMap.put(entry.getKey(), entry.getValue());
                }
                context.removeAttribute(name);
            }
        }
       
        return copyedMap;
    }


위 작업 후 동시의 1000개의 요청에 대해 테스트한 결과,
기존의 직접 디비 작업을 하는 경우 40여초가 소요된 반면
변경 후 컨텍스트에 저장하는 방식은 대략 5초 정도로 큰 성능 향상이 있었다.
반응형
댓글
공지사항