Search

비동기 적용으로 Backend 성능 개선 예제

카테고리
Back-end
태그
AWS
Python
게시일
2024/04/22
수정일
2024/04/26 08:15
시리즈
AWS-Study
1 more property

1. Intro

만약 Backend에서 외부로 API요청을 날리고 처리하는데까지 상당한 시간이 소요된다고 가정했을 때 어떻게 처리할 수 있을지에 대한 예제를 생각해보고 적용해본 후기를 작성해보고 싶어 포스트를 쓰게 되었습니다. 저는 실무에서 예를 들어 차량을 조작하는 API를 쏘고, 이에 대한 응답이 느린 경우에 이를 적용해보았습니다.

2. As-Is

기존의 Request/Response의 흐름은 다음과 같습니다. 구체적인 흐름을 작성하기보다, 핵심적인 흐름만 요약하여 작성해두었습니다. 흐름을 설명하면 다음과 같습니다.

Flow Description

고객이 앱(혹은 웹)을 통해 Backend로 API를 요청
요청한 API를 처리하기 위해서 Provider와 API Req/Resp 과정을 수행함
이 과정에서 Provider는 최대 30초 동안 처리하게 됨
maximum 30초 동안 Backend 프로세스가 점유되어 새로운 처리를 하지 못함
Provider로부터 Response를 받으면 Handler를 통해 처리 결과를 검사함
처리 결과를 반환함

Issues

이처럼 흐름 자체는 길지 않지만, Backend와 Provider 사이에 최대 요청/응답 시간이 있어 전체적인 서비스 성능에 영향을 끼치게 됩니다. 이런 경우를 해결하기 위해 비동기로 처리하는 방법을 생각해볼 수 있습니다.

3. To-Be

비동기로 처리하는 Request/Response 핵심 흐름은 다음과 같습니다. 흐름 내에서 크게 달라진 점은 Lambda Function 혹은 Celery Worker를 추가한 흐름이 들어가고, 비동기 처리를 위한 Locking 작업이 추가되었습니다. 구체적인 흐름 설명과 후기는 아래에 작성해두었습니다.

Flow Description

고객이 앱(혹은 웹)을 통해 Backend로 API를 요청
비동기 과정에서 발생하는 이슈를 예방하기 위해 API를 처리하기 전, 중복 요청이 없는지 mem cached된 Lock이 있는지 검사
Lock이 없다면 Lambda Function(혹은 Celery Worker)을 요청함
Lock이 있다면 Warning Push 발송 후 Lambda Function(혹은 Celery Worker) 로직까지 넘어가지 않음
Lock의 경우 최대 시간 (maximum 30s) 이후 자동으로 만료되도록 expire
Lambda Function(혹은 Celery Worker)에서 Provider와 통신
완료 후 Handler를 통해 처리 결과를 검사 후 Push를 반환함
Client는 Push 메시지에 따라 비동기로 응답을 처리함

Reviews

위와 같은 플로우로 처리하기 위해 Lambda Function 혹은 Celery Worker를 추가할 경우 기존 Backend의 Process가 특정 시간동안 오래 점유되는 이슈는 해결되었습니다. 다만, 백엔드에서 비동기로 처리하기 위해 Push 메시지 형태로 응답을 전달하기 때문에 프론트엔드에서 Action이 일어나는 로직을 수정해주어야 했습니다. 또한 Lambda Function과 Celery Worker를 적용했을 때의 차이점이 몇 가지 있어 이를 적어볼까합니다.

Lambda Function

Lambda를 사용하기 위해서는 boto3를 이용하여 lambda client를 선언하고 사용하는 로직을 개발해야 합니다. 또한 AWS의 내에 Lambda 설정을 해주어야 하는 번거로움이 있습니다. 하지만 장점 역시 두드러졌습니다.
필요할 때만(불필요한 중복 요청은 memory lock을 이용하여 제어 가능) Lambda Function을 요청하고, 작업에 대한 책임을 Lambda 쪽으로 옮겨 관리가 가능함
서버가 필요없는 구조기 때문에 비용적인 측면에서 장점이 있음
Scale 관리가 불필요함
Lambda를 이용하여 관리하게 되면 생각보다 많은 장점이 있습니다. 다만, Celery를 활용하게 된다면 어떨지 고려해보았습니다.

Celery Worker

Celery Worker를 이용하여 처리하기 위해서는 Celery Worker와 Beat 세팅을 별도로 해주어야 하고 이에 따른 관리가 추가적으로 필요하게 됩니다.
Celery Worker / Beat 세팅을 위한 서버작업 필요
Worker당 최대 프로세스가 있기 때문에 추후 Scale 관리가 필요할 수 있음
사용하지 않을 겨우에도 서버 비용이 발생하게 되어 비용적으로 부담이 될 수 있음

4. PoC

테스트를 위한 간단한 예제를 작성해보았습니다. Backend 내의 로직인 아닌 필수적인 로직만 스크립트로 작성하여 테스트 해보았습니다.

Backend - request lambda function

import time import json import boto3 import redis import redis_lock redis_client = redis.StrictRedis(host="localhost", port=6379, db=0) lock_name = "[LOCK-NAME]" lambda_client = boto3.client( "lambda", region_name="ap-northeast-2", aws_access_key_id="", aws_secret_access_key="" ) payload = { "registration_token": "[CUSTOMER-APP-PUSH-TOKEN]", "title": "Title from boto3", "body": "Body from boto3", } for __ in range(20): lock = redis_lock.Lock(redis_client=redis_client, name=lock_name, expire=5) if lock.locked(): print(f"Function is locked. (LOCK_STATUS : {lock.locked()})") else: lock.acquire(blocking=False) print(f"Call lambda function.") lambda_client.invoke( FunctionName="[LAMBDA-FUNCTION-NAME]", InvocationType="Event", Payload=json.dumps(payload) ) time.sleep(0.5)
Python
복사

Lambda function

import time import json import firebase_admin from firebase_admin import credentials, messaging def lambda_handler(event, context): try: firebase_admin.initialize_app(credentials.Certificate('./firebase_settings.json')) except ValueError: pass registration_token = event.get("registration_token") message = messaging.Message( notification = messaging.Notification( title=event.get("title"), body=event.get("body") ), token=registration_token, ) response = messaging.send(message) print(f"Message : {response}") time.sleep(10) return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') }
Python
복사
// firebase settings.json { "type": "[TYPE]", "project_id": "[YOUR-PROJECT-ID]", "private_key_id": "[YOUR-PRIVATE-KEY-ID]", "private_key": "[YOUR-PRIVATE-KEY]", "client_email": "[YOUR-EMAIL]", "client_id": "[YOUR-ID]", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "[YOUR-FIREBASE-SDK-URL]", "universe_domain": "googleapis.com" }
JSON
복사

5. Conclusion

이번 포스트에서는 비동기로 성능을 개선하는 케이스에 대해 알아보았습니다. 비동기를 적용하는 How에 대한 고민과 Cost(비용)에 대한 고민을 했던 후기를 아래에 작성해보도록 하겠습니다.

How?

늘 고민했던 주제로써 비동기를 적용해보았는데, 비동기를 사용하게 되면 일반적인 경우 이외에도 많은 경우를 고민해보아야 했습니다. 대표적으로 redis lock처럼 아직 처리가 되지 않았을 때 같은 요청이 오는 경우, 이를 제어하거나 비동기 요청을 어떤 형태로 처리할지 Lambda나 Celery Worker 와 같이 How를 고민해야 했습니다.

Cost?

이번 케이스를 계기로 기존에 사용했던 방법인 Worker를 사용하지 않고 Lambda를 이용하여 처리하는 방법으로 처리해보았습니다. 둘을 비교한 결정적인 부분은 비용이었습니다. Celery worker를 사용하게 되면 Worker의 스펙을 고민하고 Scale을 고민해야 하고, 특히 사용하지 않을 경우 지속적으로 서버가 유지되는 비용이 든다는 점에서 Lambda를 고민하여 적용해본 계기가 되었습니다.
비용적인 부분과 별개로, Lambda를 활용해보니 이러한 비동기 처리에 대해 좀 더 자유롭게 사용할 수 있겠구나, 하는 즐거움을 느꼈습니다. 인프라를 고민하지 않아도 되고 서버를 어떻게 구동할지도 고민하지 않아도 됐었습니다. 앞으로 이런 형태로 활용을 자주해보면 좋겠습니다. :)