본문 바로가기

Hadoop/Presto

[Presto] Nginx + Reverse proxy를 이용한 Cache Layer 구축기

현재 Tabluea + Presto + Kudu 를 이용해 BI 플랫폼을 제공하는 프로젝트를 진행중이다.

이때 Tabluea를 통해 매일 일간 지표에 대한 대시보드를 제공하고 있는데, 매번 사용자가 Tabluea에 접속할때마다 Presto 쿼리가 수행되어 대시보드 화면이 늦고, Presto & Kudu의 리소스가 낭비가 되는 문제가 있었다.

이를 해결하기 위해 Presto 앞에 Cache Layer를 하나 두고, 동일한 쿼리를 수행하려고 하면 캐싱된 값을 리턴 할 수 있도록 만들어 보고자 했다.

1. Presto-jdbc의 통신방식

Presto-jdbc를 보다보니, JDBC Driver와 Presto coordinator간에 http로 통신하는 것을 확인하였다. 즉 일반적인 http 캐싱 방식으로 처리하면 쉽게 해결할 수 있을것으로 보여 nginx를 이용하여 Cache layer를 생성하기로 결정했다.

2. Nginx 설치 및 Reverse Proxy, Proxcy Cache 설정

//nginx 설치
$ sudo yum install -y nginx

1) cache 저장소 지정

proxy_cache_path ${/local/cache/path} levels=1:2  keys_zone=query_cache:10m inactive=1h  max_size=25g use_temp_path=off;
  • ${/local/cache/path} : 캐싱된 데이터는 로컬에 파일 형태로 저장 되는데, 이를 저장하기 위한 경로를 환경에 맞게 설정한다.
  • levels: 캐싱 파일을 디렉토리 별로 저장할지 저장한다. 만약 지정하지 않으면 local cache path에 저정한 디렉토리에 모든 캐시파일이 생성된다.
  • keys_zone: 캐시의 메타 데이터가 저장될 저장 공간의 이름을 부여한다. 10m은 메모리 공간의 크기이며 1~10m로 지정가능하다.
  • inactive: 캐시가 지정된 시간 만큼 사용되지 않으며 삭제된다.
  • max_size: 최대 캐시 저장소 크기
  • use_temp_path: 해당 옵션을 키면(default=true) 캐시파일에 바로 로컬 캐시 패스에 저장되는것이 아니라, temp 공간에 저장된 뒤 로컬 캐시 패스에 복사 된다. 크게 의미 있는 작업이 아니므로 off로 꺼준다.

추가적으로 캐시 옵션을 보려면 nginx http proxy module을 확인하면 된다.

2) Reverse proxy 설정

Reverse proxy를 설정하여 nginx를 통해 Presto로 접근하도록 설정한다.

server {
      listen 443 ssl;
      server_name ${nginx domain};


      ssl_certificate ${crt_pem_path};
      ssl_certificate_key ${key_pem_path};

      ssl_session_timeout 5m;

      ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
      ssl_ciphers         HIGH:!aNULL:!MD5;

      location / {
            proxy_pass https://${presto coordinator}/;
            proxy_pass_request_headers on;
            ...
        }
    }

Presto coordinator 쪽에 Ranger 사용하기 위해 SSL 적용된 상태이다. 따라서 Nginx 도 SSL 설정을 통해 Reverse proxy를 구현해야 한다. 만약 Presto에 SSL이 적용되지 않았다면 일반적인 Reverse Proxy 설정을 통해 proxy_pass를 설정하면 된다.

3) Proxy Cache 설정

proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie;
proxy_set_header Accept-Encoding "";
proxy_cache query_cache;
proxy_cache_methods POST;
proxy_cache_min_uses 1;
proxy_cache_key "$request_uri$request_body";
proxy_cache_valid 200 1h;
  • proxy_cache : 1번에서 지정한 keys_zone의 이름을 지정한다.
  • proxy_cache_methods : Http method 중 어떠한 요청을 캐싱할껀지 지정한다. Presto는 쿼리 수행시 모두 Post로 진행함으로 Post 일때만 캐싱 되도록 설정했다.
  • proxy_cache_min_uses: 동일한 요청이 몇번이상 들어오면 캐싱할것인지 지정한다. 기본값 1
  • proxy_cache_key: 캐시 키를 지정한다. 쿼리 수행시 http body에 쿼리 문자열이 전송된다. 그래서 request_uri와 request_body(쿼리 문자열)의 조합으로 해시 키를 설정하였다.
  • proxy_cache_valid: http 상태값에 따라 캐시 보관 주기를 지정할 수 있다. http status == 200 OK 일때 1시간동안 캐싱된 데이터를 보관한다.

4) sub_filter를 통한 Response body Replace 처리

sub_filter_once off;
sub_filter_types application/json;
sub_filter '${preso-coordinator-host}' '${nginx_domain}';

Content-type이 application/json 일때 Response body의 ${preso-coordinator-host}를 ${nginx-domain}로 바꾸겠단 뜻이다.

Response Body를 Replace 하는 이유

위 작업을 왜 해야 하는지 이해하려면 프레스토가 쿼리를 실행하고 결과를 받아오는 방법을 알아야 한다.

Presto-jdbc 통신방식에서도 소개했지만, Presto-jdbc는 Http를 통해 쿼리를 수행하고 결과를 받아본다.

이때 http의 stateless라는 특성상 한번의 http request로 쿼리 결과를 받아 올 수 없다.
stateless 라는 것은 client-server간 요청이 발생했을때 생성된 커넥션이 계속 유지되는것이 아니라, 요청이 발생할 때마다 커넥션을 열고/닫는 것을 말한다.
만약 한번의 http request로 쿼리를 수행하고 결과를 받아보려면 쿼리가 실행되는 시간동안(빠르면 수초, 느리면 수분동안) http 커넥션을 맺고 있어야 한다.
이렇게 되었을때 쿼리 수행시간이 길어지면 timeout 관련 설정에 따라 쿼리 결과를 리턴 받지 못했음에도 커넥션이 스스로 닫혀 쿼리 결과를 얻지 못할 수 있다.

이를 해결하기 위해 Presto Driver는 여러번의 http request를 수행하고, 계속 쿼리 수행에 대한 상태를 추적하여, 결과가 도출되었을때 해당 결과를 가져오도록 설계되어있다.

그렇다면 쿼리 수행상태는 어떻게 확인할까?

이는 실제 nginx를 통해 캐싱된 데이터를 보면 알수 있다.

{
    "id": "20200716_052725_00804_n66e8",
    "infoUri": "https://presto-coordinator-host/ui/query.html?20200716_052725_00804_n66e8",
    "nextUri": "https://presto-coordinator-host/v1/statement/queued/20200716_052725_00804_n66e8/ye6bc1f2c52b8d9e00a5849a9c898d490ebd78814/2",
    "stats": {
        "state": "QUEUED",
        "queued": true,
        "scheduled": false,
        "nodes": 0,
        "totalSplits": 0,
        "queuedSplits": 0,
        "runningSplits": 0,
        "completedSplits": 0,
        "cpuTimeMillis": 0,
        "wallTimeMillis": 0,
        "queuedTimeMillis": 0,
        "elapsedTimeMillis": 0,
        "processedRows": 0,
        "processedBytes": 0,
        "peakMemoryBytes": 0,
        "spilledBytes": 0
    },
    "warnings": []
}

결과를 보면 nextUri라는 필드가 있는데, 이 uri를 호출하여 쿼리 수행상태를 주기적으로 체크하게 되는 것이다.

만약 http request가 3번 수행이 된다고 가정해보면
첫번째 요청은 정상적으로 nginx 도메인으로 요청이 전달되어 캐싱이 되지만, 2~3번 요청은 nextUri를 값을 통해 체크하게 되므로, 2,3번 요청은 정상적으로 캐싱이 되지 않게 된다.(바로 presto coordinator로 요청이 되기 때문에)
따라서 sub_filter를 통해 반드시 response body의 presto coordinator 도메인을 nginx 도메인으로 바꿔주는 작업을 해야하는 것이다.

5) 쿼리 길이에 따른 캐싱 처리

위의 작업 까지만 하더라도 일반적인 상황에서의 캐싱 처리는 잘 된다. 하지만 쿼리 길이가 길어지면 캐싱이 안되는 문제를 확인하였는데, 이를 해결 하기 위해 request body 크기를 늘려준다.

client_max_body_size 12m;
client_body_buffer_size 12m;
proxy_buffers 8 2m;
proxy_buffer_size 12m;
proxy_busy_buffers_size 12m;

3. jdbc를 통한 연결

jdbc connection url을 presto coordinator에서 nginx 로 바꿔주기만 하면 된다

기존: jdbc:presto://presto-coordinator:8443
Cache Layer 적용: jdbc:presto://nginx-cache:443 (SSL 기본 포트인 443으로 연결)

nginx.conf

...중략...

http {
  server {
        listen 443 ssl;
        server_name ${nginx_domain};


        ssl_certificate ${crt_pem_path};
        ssl_certificate_key ${key_pem_path};

        ssl_session_timeout 5m;

        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;

        location / {
              proxy_pass https://${presto coordinator}/;
              proxy_pass_request_headers on;

              proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie;
              proxy_set_header Accept-Encoding "";
              proxy_cache query_cache;
              proxy_cache_methods POST;
              proxy_cache_min_uses 1;
              proxy_cache_key "$request_uri$request_body";
              proxy_cache_valid 200 1h;

              sub_filter_once off;
              sub_filter_types application/json;
              sub_filter '${preso-coordinator-host}' '${nginx_domain}';

              client_max_body_size 12m;
              client_body_buffer_size 12m;
              proxy_buffers 8 2m;
              proxy_buffer_size 12m;
              proxy_busy_buffers_size 12m;
          }
      }
}
...중략...