<개요>
- 다음과 같이 Service #A 에서 Service #B로 데이터 조회 API를 요청하고 값을 받아오는 로직이 있다.
- Service #B에서는 AWS Athena를 저장소로 사용하고 있으며 Athena JDBC42 드라이버를 사용 중 이다.

API 호출 후 응답


<현상>
- Service #B에서 JdbcTemplate을 통하여 쿼리가 수행된 시간은 11:13:13 이고,
2021-11-04 11:13:13.482 DEBUG 9668 --- [http-nio-8200-exec-9] o.s.jdbc.core.JdbcTemplate : Executing SQL query
2021-11-04 11:13:13.482 DEBUG 9668 --- [http-nio-8200-exec-9] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
- 실제 쿼리 수행결과를 받아온 시간은 11:15:57 로 약 2분44초 가 소요되었다.
2021-11-04 11:15:57.998 INFO 9668 --- [http-nio-8200-exec-9] ...

- Athena 의 경우 동시에 다수의 쿼리가 수행되면 Queue에 의하여 순차적으로 수행될 수 있기 때문에 쿼리 히스토리를 조회하였다.

11:13:13.542초 시작, 수행시간 0.555초
대기열시간 1분21초

- 대기열 시간 1분21초 + 수행시간 0.555초를 제외하고 꽤 오랜시간이 소요되었다.

<소스분석>
- AthenaJDBC42의경우 일반적인 JDBC드라이버처럼 커넥션을 맺고 Resultset을 처리하는 형태가 아니라 AWS Athena로 Http를 통해서 수행요청을 하고, 리턴값으로 ID를 받아온 뒤 일정시간 Thread Sleep하면서 조회 polling을 요청하고 Status가 Completed가 되었을때 후속처리를 하는 형태로 구성되어 있다.

- 또한 위에도 언급한것처럼 동시에 다수의 요청이 집중될경우 자체적으로 큐에 보관하여 처리하게 된다.

- 부수적으로 Athena JDBC드라이버의 SStatement내 execute, getResultSet등의 메소드를 살펴보면 대부분 synchronized로 선언이 되어있기 때문에 이에 따른 delay도 있지 않을까 예상한다.

 

<Thread Dump>

10개의 Thread가 같은 위치에서 대기중이다.

"http-nio-8200-exec-9" #44 daemon prio=5 os_prio=31 tid=0x00007ffcc655f800 nid=0x8c03 waiting on condition [0x000070000c638000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.simba.athena.athena.api.AJClient.executeQuery(Unknown Source)
at com.simba.athena.athena.dataengine.AJQueryExecutor.execute(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeNoParams(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeNoParams(Unknown Source)
at com.simba.athena.jdbc.common.SStatement.executeQuery(Unknown Source)
- locked <0x000000078740ccf8> (a com.simba.athena.athena.jdbc42.AJ42Statement)
at com.zaxxer.hikari.pool.ProxyStatement.executeQuery(ProxyStatement.java:111)
at com.zaxxer.hikari.pool.HikariProxyStatement.executeQuery(HikariProxyStatement.java)
at org.springframework.jdbc.core.JdbcTemplate$1QueryStatementCallback.doInStatement(JdbcTemplate.java:439)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:376)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:452)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:462)
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:473)
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:480)


<정리>
- 다수의 사용자에게서 발생하는 ad-hoc형태 처리는 적합하지 않다.(hive와 동일함)

- Global cache(Redis)를 적절히 활용하여 Service #B Layer에서 처리를 하도록 하면 효율성을 증가시킬수 있다.(일반적인 캐시전략)

- Red Shift등의 빠른대안도 있으나 가성비가 매우 떨어진다.


Posted by 멋진그이름 멋진그이름

댓글을 달아 주세요

<현상>

- Spring @Async를 사용하여 비동기 메소드를 작성하고 기존에 CompletableFuture를 사용해서 값을 넘겨받던 부분이 어느순간부터 null 로 넘어오는 현상

기존에 retMap이 넘어오던 부분이
null 로 처리가 되기 시작한다.

 

<원인>

- 한참 디버깅중에 원인을 찾았다. 기존에 Logging Aspect를 이용하여 @Async 메소드에 대해서 로깅처리하던 부분이 있었다.

- 기존에 @After advice를 활용하던부분을 @Around advice로 변경하면서 발생하는 문제였다.

- @Around를 사용할 경우 proceed 를 명시적으로 수행해야만 다음단계로 넘어가게 되는데, 리턴값을 전달해주어야 한다.

 (이런 초보적인 실수를  ㅠㅠ)

    @Around("...PointCut()")
    public Object logger(ProceedingJoinPoint pjp) throws Throwable {
   
        log.debug("before {}", ...);
        Object obj = pjp.proceed();
        log.debug("after {}", ...);
        return obj;
    }

 

Posted by 멋진그이름 멋진그이름

댓글을 달아 주세요

https://icthuman.tistory.com/entry/AWS-S3-Athena-%EC%82%AC%EC%9A%A9%EC%A4%91-JDBC-Driver%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C

 

AWS S3-Athena 사용중 JDBC Driver동시성 문제

<개요> - S3보안버켓으로 파일을 올려서 Athena 작업중 - 1개의 CSV파일(2.65GB) 12개의 컬럼 - AthenaJDBC42.jar 사용 s3.amazonaws.com/athena-downloads/drivers/JDBC/SimbaAthenaJDBC-2.0.16.1000/docs/Simba+..

icthuman.tistory.com

<현상>

- AWS Athena사용시 quota제한 으로 인하여 다음과 같은 문제가 발생하였습니다.

- 동시에 많은 요청이 몰리는 것을 방지하기 위해서 다음과 같이 API호출수를 제한하도록 하였으며 object wait/notify를 활용하였습니다.

 

<로직>

- API 호출시 기본적으로 @Async호출

- API호출전에 현재 호출count를 확인하여 일정수치 이상일 경우 object.wait()로 대기

- 호출이 성공하면 count를 증가

- 비동기호출을 마치고 응답을 받으면 callback으로 호출count를 감소시키고 object.notify

CompletableFuture<Map<String, Integer>> completableFutureForRule;
    ...
    synchronized (this){
		waitBeforeCall();
		completableFutureForRule = apiCallService.call(request);
		resultCount.incrementAndGet();
	}
    
    completableFutureForRule.thenAccept(retMap -> {
    	synchronized (this){
    		notifyAfterCall();
    }
private void waitBeforeCall(){

	try {
  
		while (apiCallService.getCallCount() >= SIZE) {
			this.wait();
		}
	} catch (InterruptedException e) {
		log.error("Interruped");
	}
	apiCallService.incrementAndGet();
}

private void notifyAfterCall(){
	apiCallService.decrementAndGet();
	this.notifyAll();
}

- blocking queue를 구현할때 와 매우 유사한 로직임을 알수 있습니다.

 

<참고사항>

- caller service 와 callee service가 같은 비율로 증가한다면 이론상 문제는 없습니다. (e.g 10, 20, 30)

  그러나 실제는 두 서비스간에 연관관계가 없을 것으로 예상되며, 또한 이 로직은 한 jvm내 단일 service에서만 유용하기 때문에 ECS와 같이 scale out 이 되는 구조에서 전체 API 호출수를 제한하기 위해서는 다른 방법을 사용해야 합니다. 

e.g) A. cluster shared lock

      B. API gateway를 활용한 limit rate

      C. Redis를 활용한 global count개념

 

- wait후 notify의 경우 잠들어 있는 하나의 쓰레드만 깨우기 때문에 여러가지 예외 상황에 대응하거나 개별제어가 어려워서 일반적으로 notifyAll을 많이 사용합니다.

- while()문으로 체크해야 하는 이유는 여러 쓰레드가 다시 lock을 획득하기 위해 경쟁하기 때문입니다. 또한 Spurious wakeups 처럼 이유없이 쓰레드가 깨어난 경우에도 다시 wait로 진입하기 위해서 필요합니다.

- 이와 같이 일정시간 대기하는 로직을 작성할때 sleep을 사용하는 경우를 간혹 볼 수 있는데, sleep의 경우 wait와 다르게 lock을 반환하지 않으므로 주의해서 사용해야 합니다. (stackoverflow 참조)

https://stackoverflow.com/questions/1036754/difference-between-wait-and-sleep

- 로직이 복잡해질 경우 CountDownLatch 를 이용해서 구현하는 것을 권장합니다!

Posted by 멋진그이름 멋진그이름

댓글을 달아 주세요