본문 바로가기
AWS/Lambda

Lambda를 활용한 저비용 분산 캐시

by 알파해커 2020. 4. 6.
반응형

올 2월, USENIX에서 발표된 아주 따끈따끈한 논문을 하나 소개하고자 한다.

제목은 "InfiniCache: Exploiting Ephemeral Serverless Functions to Build a Cost-Effective Memory Cache"

 

뭐 어려운 말로 적혀있는 것 같지만, 요약하면, "Lambda를 활용한 저비용 분산 캐시" 쯤 될 것 같다.

Lambda를 왜 활용하는 걸까. 무슨 이득이 있을까. 

 

AWS에서 (분산) 캐시를 사용하는 가장 간단한 방법은 Managed Service인 ElastiCache를 사용하는 것이다.

클라이언트는 ElastiCache를 쓰기 위해서 특별한 노력이 필요하지 않고, 그저 인스턴스를 생성해서, 만들어진 Endpoint를 통해 명령을 보내고 결과를 받으면 된다.

 

 

문제점

간단하다. 그럼 뭐가 문제일까?

 

1. 비싸다.

대부분들의 (분산) 캐싱 솔루션들은 VM에서 클러스터를 만들어 동작한다. 그리고 비용이 메모리 양에 따라 정해져 있다. 얼마나 많이 사용했는지와 관계 없이. 근데 그 비용이 생각보다 만만치가 않다. 실제로 우리 조직에서도 리소스 비용으로 가장 많이 나오는 것 중의 하나가 ElastiCache 이다.

 

2. 큰 데이터는 캐싱이 안된다.

기존의 솔루션에서 메가 바이트 이상의 큰 데이터에 대해 캐싱을 잘 안했던 이유는 다음과 같다.

    • 별로 자주 액세스 하지 않은데도, 메모리를 많이 잡아 먹음.

    • 심지어는 그렇게 메모리를 잡아먹는거 때문에, 걔보다 자주 사용되는 작은 사이즈의 데이터들이 Eviction 됨.

    • 큰 데이터를 가져올 때 Network Bandwidth를 많이 점유해서(Network Contention 상승), 다른 작은 데이터(일반적인 데이터)를 가져갈 때 성능에 악영향을 끼침.

 

Lambda

그럼 어떻게 해결할 수 있을까.

 

우리가 잘 아는 AWS Lambda를 사용하는 것이다.

Lambda의 과금 방식은 pay-per-request로 사용한 만큼 비용이 발생(실행시간 100ms 단위로 과금)하게 되어 있고, Intensive 하게 요청을 날리는 환경이 아니라면 분명 이득이 있을 것이다.

 

 

Lambda 캐싱의 한계

그럼 이제 Lambda 컨테이너 하나 띄워서 캐싱했다고 가정해보자. 컨테이너가 하나만 떠있다면 다음과 같은 한계점이 존재할 것같다.

 

1. 최대 캐싱 사이즈가 3GB 밖에 안됨

Lambda의 최대 메모리는 3GB이다.

 

2. 병렬 처리가 안됨

람다는 한번에 하나의 요청만 처리한다. 동시에 들어오는 요청은 컨테이너를 하나 더 띄워서 처리하게 되어 있다.

 

3. 그럼 원래 Lambda 성격대로, 컨테이너가 여러 개 뜬다면?

새로 뜨는 컨테이너에는 당연히 캐싱이 되어있지 않을 것이고, 따라서 DB에서 데이터를 가져와서 다시 캐싱할 것이다.

이런 식으로, 컨테이너가 뜰때마다 다시 캐싱 해줘야 할 것이다. (다시말해, 캐싱의 효과가 전혀 없다는 것)

 

표현하자면 아래 그림과 같다. 

 

 

그리고, 그렇게 캐싱 되더라도, 클라이언트 관점에서 어떤 Lambda 컨테이너에 원하는 데이터가 캐싱이 되어있는지 알 수가 없다.

 

4. 리소스의 한계: 1 or 2 CPU, 최대 3GB 메모리, 제한적인 네트워크 대역폭

특히 네트워크 대역폭(Network Bandwidth) 문제부터 이야기 하자면. Lambda 컨테이너는 VM위에 올라가는데, AWS에서는 최대한 하나의 VM 위에 여러 컨테이너를 올리려고 시도한다. 만약에 하나의 VM 위에 저사양의 Lambda가 여러 개가 떠서 활발한 통신을 하게 되면(클라이언트 로직 자체가 통신을 많이 하는 것이거나, 파일 사이즈가 크거나 등등), 자원 경쟁*(Network Contention)이 심해져서, 성능에 문제가 발생할 수 있는 것이다.

 

5. 떠있는 컨테이너는 언젠가 반환(Reclaim) 됨

Lambda 컨테이너는 계속 떠있을 수 없고, 언젠가는 반환되게 되어 있다. 우리가 선택할 수 있는 문제가 아니다. (캐싱된 데이터가 언젠가 무조건 모두 날라간다는 말).

 

6. Lambda 최대 실행 시간의 제한: 15분

batch 성 프로세스는 돌리기 쉽지 않을 것이다. 처리를 오래해야하는 무진장 큰 데이터도 좀 힘들겠지.

 

7. Lambda는 Outbound Network Connection만 허용함

일반적인 서버로 존재할 수 없다는 의미.

 

8. 100ms 단위의 과금 체계

pay-per-request가 문제라는 것이 아니라, 무조건 100ms이 단위라는 것이 문제라는 것이다.

가령, 4ms, 5ms 만에 실행이 끝나도 100ms 만큼의 비용이 발생한다. 

 

 

적고보니, 해결책이 될 수 있을까 싶을 정도로 많다;;

 

* 네트워크 자원경쟁 관련해서..

참고로, Lambd 컨테이너의 메모리 사이즈는 클수록 좋다. (Lambda 메모리 = 128MB~ 3GB)

  • Lambda의 CPU는 메모리에 비례해서 올라간다. 즉, 높은 메모리를 쓰면 높은 CPU가 할당된다

  • 본 논문에서 실험해본 결과 1.5GB 이상의 메모리를 사용했을때, 각 Lambda 컨테이너가 서로 다른 VM에 호스팅 되는 것을 확인했다. 서로 다른 VM에 호스팅 되어야, Network Contention을 줄일 수 있고, Latency의 Variation과 예상치못한/불필요한 성능 저하를 줄일 수 있다.

 

 

해결책: InfiniCache

논문에서는 아래와 같은 구조를 제안한다.

Proxy를 하나 두어서, 클라이언트(그림에서는 Application이라고 표현한 것)와 Lambda 들 사이에서 중재자 역할을 하게 하는 것이다. 그리고 그 Proxy는, 클라이언트 관점에서, 캐시 솔루션의 Endpoint가 된다.

 

다르게 표현하면, Proxy 하나당 캐싱할 수 있는 Lambda Function을 여러개 가지고 있는 것이다.

이 때 특이한 점은, Lambda Container가 아니고, Lambda Function을 여러 개 가지고 있는 것이고, 각 Lambda Function의 Container는 (Reclaim 전 데이터 Back up 때만 빼고) 항상 하나만 유지한다.

 

 

 

Proxy

앞서 언급한 것과 같이 Lambda 자체만으로는 다양한 한계점이 존재한다.

때문에, 그러한 문제점들을 해결하기 위해 Proxy가 많은 역할을 해줘야할 것이다.

 

Proxy는 다음과 같은 특징과 역할을 가진다. 

 

1. 언제나 떠있어야 한다.

Stateful하게 Lambda 컨테이너들의 상태를 항상 저장해두고 있어야 하니까. 그리고, 클라이언트가 액세스하기 위한, 고정적인 Endpoint 가 필요하니까. EC2 인스턴스라고 상상하자.

 

* 이때 클라이언트란, 외부 사용자를 의미하는 것이 아니다. 위의 그림에 표현되어 있는 것 처럼, 비즈니스 로직이 돌고 있는 Compute Layer의 서버를 의미하는 것이다.

 

2. 중재자

클라이언트로 부터 들어오는 데이터를 Lambda에 캐싱하고, 어느 Lambda에 캐싱이 되었는지 기록한다. 그리고, 각 Lambda의 메모리 관리를 하고, 캐싱된 Lambda 컨테이너가 회수(Reclaim)되서 데이터가 유실되지 않도록 지속적으로 관리하고 백업도 해줘야 한다.

 

- Lambda Pool 관리: 각 Proxy에는 자신에게 할당되어 있는 정해진 Lambda들이 있다. 그리고, Proxy는 자신과 연결되어 있는 Lambda 컨테이너가 계속 떠있을 수 있게 유지 + 계속 떠있을 수 없는 상황이라면 다른 Lambda 컨테이너를 새로 띄워서 데이터를 back up 한다 (해당  컨테이너에 데이터가 캐싱되었을 경우에).

 

-메타 데이터 관리: 클라이언트로 부터 들어온 데이터를 Lambda로 전달한 후, 캐싱이 잘되고 나면, 어느 Lambda에 캐싱이 되었는지 mapping 테이블에 기록 해놓는다.

 

- 메모리 관리: 각 Lambda의 메모리 상황을 체크해서 LRU 기반 Eviction Policy로 메모리 관리를 한다.

 

 

Proxy도 여러 개 떠있을 수 있다(아니, 꼭 그래야만 할 것 같다. SPOF도 피해야 할테니까). 이 때, 클라이언트가 이 다수의 프록시 EC2 인스턴스 중 어느 곳에 붙어야할지(어느 프록시와 통신해서 데이터를 보낼지) 결정하게 될 것이다. 여기에 적용되는 방식은 Consistent Hashing을 적용한다. 그리고 그 알고리즘은 클라이언트가 사용하는 라이브러리에 포함되어 있다(위 그림의 Client Library).

 

조금 소규모의 경우에는 클라이언트와 프록시를 한 몸으로 만들 수도 있을 것이다. Network Hop을 조금이라도 줄여주기 위해서 (대신, 클라이언트도 Stateful 한 형태의 인스턴스여야 겠지)

 

 

Backward Connection

Proxy와 Lambda는 어떻게 통신할까.

이 둘이 통신을 할때도, (원래 Lambda가 그러하듯이,) Stateless한 통신을 하면 두 가지 측면에서 비효율이 발생할 수 있다.

 

1. 앞서 언급했듯이, 실행 시간으로 몇 ms 를 사용했든, 무조건 100ms 단위로 과금된다.

만약에 내가 4ms만 사용했어도 100ms으로 과금되니까, 96ms은 버리는거다.

 

2. 하나의 요청에 대한 처리가 완전히 끝나기 전까진 해당 컨테이너는 Block이 되니까, 성능상 이슈가 발생할 수 있다.

가령, 큰 데이터(사진이나 파일 같은)를 보낼 때,  Blob 데이터를 여러 요청으로 쪼개서 보낸다고 가정해보자. 매번 쪼개진 데이터를 모두 각각의 요청으로 보낸다면, 과금 손해와 Blocking에 의한 손해를 보게 될 것이다. 따라서, 한번에 여러 리퀘스트를 보내서 이득을 보는 편이 나을 것이다.

 

이러한 이유로, TCP Connection을 이용한다.

통신하고자 하는 Lambda 컨테이너와의 첫 연결(Invocation)만 Stateless하게 통신하고, 나머지는 TCP 통신해서 위의 문제점을 해결한다. (TCP Connection의 자세한 과정은 바로 뒤에서..)

 

 

Lambda Caching Pool

Lambda 컨테이너는 영원이 떠있을 수 없다. 주기적으로 무조건 회수(Reclaim) 된다. 다시말해, 어느 순간 컨테이너가 내려가고, 해당 컨테이너에 있던 데이터가 모두 유실된다는 것이다.

 

그나마 다행인 것은, 이런 Reclaim 작업은 주기적으로 일어난다.

(실험에 따르면, 1분 정도로 invocation을 꾸준히 해줬을 때, 6시간 정도에 한번씩 Reclaim 된다고 한다)

때문에, Reclaim이 발생하기 전에, 새로운 컨테이너를 띄워서 백업을 하고, Proxy와의 연결을 해당 컨테이너로 바꿔야 한다. 

 

 

백업 절차는 위의 그림과 같다.

 

("s": 원래 데이터를 가지고 있는 Lambda 컨테이너, "d": 백업을 할 대상이 되는 Lambda 컨테이너, "Relay": Proxy와 같은 리소스 상에 있는 다른 프로세스. Lambda 컨테이너 간의 데이터 교환이 이루어질 수 있도록 중간자 역할을 함)

 

1. 백업 시간이 되면, Lambda에서 Proxy로 백업을 시작하자는 신호를 보냄

2. Proxy는 Relay에게 백업 시작을 알리는 신호를 보냄 (Relay: 두 개의 Lambda 컨테이너 간의 데이터 전송 중개자)

3. Relay 자신의 네트워크 정보 (IP, port)를 Proxy에 알려줌

4. 기존의 Lambda 컨테이너에게 그 네트워크 정보를 보내줌

5. 기존의 컨테이너는 그 네트워크 정보를 이용하여 Relay와 커넥션 맺음 

6. 새로운 Lambda 컨테이너를 Invoke(자기 자신을 Invoke)하는데, 이때, 아까 받은 Relay 네트워크 정보를 같이 보냄

7. 새로운 컨테이너는 받은 네트워크 정보로 Relay와 커넥션을 맺음

8. 컨테이너 간 커넥션이 (간접적으로) 맺어졌음을 확인

9. 새로운 컨테이너와 Proxy 간의 커넥션 맺음

10. 기존 컨테이너와 Proxy간의 커넥션을 끊음

11. 기존 컨테이너에 있던 데이터를 새로운 컨테이너로 옮김

 

 

Evaluation

 

Latency

 

Cost

 

Future directions

최근에 AWS Lambda의 Provisioned concurrency 라는 서비스가 출시되었다. 항상 문제가 되던 Cold Start를 해결하기 위해, 컨테이너를 미리 동시에 띄워놓겠다는 거다. 그렇다고 해도 여전히 주기적으로 Reclaim 되고 초기화 되겠지만, Provisioned 되지 않은 컨테이너에 비해서 그 빈도수가 상대적으로 적을 수는 있다.

 

그러나, Provisioned concurrency를 사용하는 단위 시간당 비용이 EC2와 비교해서 어떤지는 확인해 볼 일이다. 적절히 hybrid로 활용할 수 있는 방법에 대해 고민해보면 좋을 듯하다.

 

Discussion and Opinion

- 당연하겠지만, pay-per-request 구조는 요청이 일정량 이상 들어오면, 일반 종량제 인스턴스보다 비용이 더 많이 발생하게 된다. 그런 구간이 반드시 있는데, InfiniCache와 ElastiCache와의 관계에도 마찬가지다. 위의 그림에서 처럼, 요청이 늘어나다가 어느 시점에 InfiniCache의 Cost가 ElastiCache를 추월한다. 실험에서는 Lambda 1.5GB 400개와 ElastiCache cache.r5.24xlarge 기준, 초당 86개의 요청이 들어올 때부터 추월했다고 한다.. 

 

- Cost 측정 결과에 EC2 (Proxy를 위한) 비용이 추가가 안되었는데, 사실 저 비용이 합쳐지면, 결과 그래프로 나타난 만큼 차이나지는 않을 것이다 (그래도 상당한 이득이 있긴 하겠지).

 

- ElastiCache를 바로 access하는 것보다, hop이 더 생겨서 성능 저하가 더해질거라 생각했는데, 사실상 별 차이가 없다.

아마도 같은 VPC내에 있어서 그런게 아닐까 추측해본다. 이 말을 다시 생각해보면, 우리가 마이크로서비스를 구축해서 같은 VPC 내에서 통신한다면, 웬만큼 통신하지 않는 이상 별 영향 없지 않을까 생각해본다.

 

- AWS에서 Lambda를 변경하지는 않을까. 일단 Lambda는 블랙박스라, 우리같이 사용자 입장에서는 자세한 내부 동작을 파악할 수 있는 방법이 제한적이다. 또한, 변경이 생겼을 때, 즉각 대처하기도 쉽지 않다. 갑자기 AWS에서 Lambda 내부 동작을 변경함으로 인해, 캐싱 시스템에 심각한 오류가 생길 가능성도 배제할 수 없다.

 

 

References

[1] Wang, Ao, et al. "InfiniCache: Exploiting Ephemeral Serverless Functions to Build a Cost-Effective Memory Cache." 18th {USENIX} Conference on File and Storage Technologies ({FAST} 20). 2020.

[2] https://mikhail.io/2020/03/infinicache-distributed-cache-on-aws-lambda/, "InfiniCache: Distributed Cache on Top of AWS Lambda (paper review)"

 

반응형

댓글