- Published on
EKS에서 413 없이 502? gRPC 최대 메시지 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 큰 요청/응답을 거부하면 보통 HTTP 413 Payload Too Large를 기대합니다. 그런데 EKS에서 gRPC를 쓰면 413이 아니라 502(Bad Gateway) 로만 보이고, 심지어 클라이언트에는 UNAVAILABLE 같은 gRPC 상태로만 떨어지는 경우가 흔합니다.
이 글은 “EKS + Ingress(또는 ALB) + gRPC” 조합에서 최대 메시지 제한(max message size) 때문에 502가 발생하는 전형적인 패턴을 재현하고, 어디를 어떻게 올려야 하는지(그리고 올리면 안 되는 지점은 무엇인지)까지 한 번에 정리합니다.
> 프록시/로드밸런서 뒤에서 스트리밍·버퍼·타임아웃 문제가 섞이면 증상이 더 복잡해집니다. 비슷한 결의 ‘프록시 뒤에서 끊김/버퍼링’ 체크리스트는 FastAPI Uvicorn에서 SSE 웹소켓 LLM 스트리밍이 프록시 뒤에서 끊길 때...도 참고하세요.
왜 413이 아니라 502로 보일까?
gRPC는 HTTP/2 위에서 동작하고, 메시지는 HTTP 바디라기보다 프레임 단위로 전달됩니다. 중간 프록시(예: NGINX Ingress, Envoy, ALB)가 메시지 크기 제한을 넘는 프레임/스트림을 만나면:
- “클라이언트가 너무 큰 바디를 보냈다” 같은 413을 정확히 반환하지 못하고
- 업스트림 연결을 끊거나(RESET/GOAWAY)
- 내부적으로
upstream prematurely closed connection류의 오류로 처리하면서 - 최종적으로 클라이언트에는 502(혹은 gRPC
UNAVAILABLE)로 나타납니다.
즉, 문제의 본질은 ‘메시지 크기 제한’인데, 관측되는 현상은 ‘게이트웨이 오류’ 로 위장되는 케이스입니다.
증상 패턴: 로그/에러에서 이렇게 보인다
1) 클라이언트(예: grpcurl, 앱 로그)
rpc error: code = Unavailable desc = upstream connect error or disconnect/reset before headersreceived RST_STREAM with code 2(INTERNAL_ERROR)transport is closing
2) NGINX Ingress Controller 로그
upstream prematurely closed connection while reading response header from upstreamclient intended to send too large body(HTTP/1.1 경로일 때만 비교적 잘 보임)- gRPC일 때는 애매한 502만 남는 경우도 많습니다.
3) 애플리케이션(서버) 로그
- 서버 프레임워크에 따라
RESOURCE_EXHAUSTED: Received message larger than max같은 명확한 로그가 남기도 합니다.
가장 흔한 원인: 제한이 ‘한 군데’가 아니라 ‘여러 군데’ 있다
EKS에서 gRPC가 지나가는 경로는 보통 아래 중 하나입니다.
- (권장) ALB Ingress Controller + gRPC: ALB가 HTTP/2를 종단/패스스루 하며 타겟 그룹으로 전달
- NGINX Ingress Controller + gRPC: NGINX가 HTTP/2를 받아 업스트림으로 프록시
- Service Mesh(Envoy/Istio/App Mesh) + Ingress: Envoy 사이드카/게이트웨이가 추가
그리고 메시지 크기 제한은 보통 다음 레이어에 각각 존재합니다.
- 클라이언트 라이브러리:
max_send_message_length,max_receive_message_length - 서버 라이브러리:
max_receive_message_length,max_send_message_length - 프록시/Ingress: NGINX
client_max_body_size(HTTP/1.1), gRPC 관련 버퍼/프레임 처리, Envoymax_grpc_message_length - (옵션) WAF/보안장비: 요청 바디 검사/제한
따라서 “Ingress만 올렸는데 그대로” 혹은 “서버만 올렸는데 그대로”가 매우 흔합니다.
재현: grpcurl로 큰 메시지 보내서 502 만들기
아래는 단순 재현 예시입니다.
# 10MB 정도의 더미 JSON을 만들어 gRPC 요청 바디에 넣는 예시
python - <<'PY'
import json
payload = {"data": "x" * (10 * 1024 * 1024)}
print(json.dumps(payload))
PY > big.json
# grpcurl로 호출 (TLS/호스트는 환경에 맞게)
grpcurl -v \
-H 'content-type: application/grpc' \
-d @ \
your.grpc.endpoint:443 \
package.Service/Method < big.json
- 제한에 걸리면 HTTP 레벨에서는 502로 보이고
- gRPC 레벨에서는
UNAVAILABLE/INTERNAL로 뭉개져 보일 수 있습니다.
해결 전략: “가장 작은 상한”을 찾아 올려라
핵심은 단순합니다.
- 현재 경로에서 가장 낮게 설정된 max message 크기가 병목입니다.
- 병목이 프록시인지, 서버인지, 클라이언트인지 먼저 확정해야 합니다.
아래는 EKS에서 자주 쓰는 조합별 해결책입니다.
1) 애플리케이션(서버/클라이언트) max message 설정
Python (grpcio) 서버 예시
import grpc
from concurrent import futures
MAX_MSG = 50 * 1024 * 1024 # 50MB
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
options=[
("grpc.max_receive_message_length", MAX_MSG),
("grpc.max_send_message_length", MAX_MSG),
],
)
# add_servicer_to_server(...)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
Python (grpcio) 클라이언트 예시
import grpc
MAX_MSG = 50 * 1024 * 1024
channel = grpc.insecure_channel(
"your-service:50051",
options=[
("grpc.max_receive_message_length", MAX_MSG),
("grpc.max_send_message_length", MAX_MSG),
],
)
# stub = ...
서버만 올리고 클라이언트를 안 올리면 응답이 큰 경우 여전히 깨집니다. 반대로 클라이언트만 올리고 서버를 안 올리면 요청이 큰 경우 깨집니다.
Go gRPC 서버 예시
import (
"google.golang.org/grpc"
)
const maxMsg = 50 * 1024 * 1024
s := grpc.NewServer(
grpc.MaxRecvMsgSize(maxMsg),
grpc.MaxSendMsgSize(maxMsg),
)
2) NGINX Ingress Controller에서 gRPC 크기/버퍼 관련 설정
NGINX Ingress는 HTTP/1.1 바디 제한(client_max_body_size)이 유명하지만, gRPC에서는 “바디” 개념이 다르게 흘러서 버퍼/프록시 설정이 함께 영향을 줍니다.
(A) Ingress annotation 예시
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-ingress
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
# HTTP/1.1 경로를 같이 쓰는 경우에만 직접적 의미가 큰 편
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# 큰 헤더/메타데이터가 있을 때도 502로 보일 수 있어 함께 조정
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
spec:
ingressClassName: nginx
rules:
- host: your.grpc.endpoint
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grpc-svc
port:
number: 50051
(B) ConfigMap에서 전역 설정(운영에서 더 일관적)
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
proxy-body-size: "50m"
proxy-buffer-size: "16k"
proxy-buffers-number: "8"
주의할 점:
proxy-body-size는 gRPC에서 “정답 열쇠”가 아닐 수 있습니다. 그래도 HTTP/JSON fallback이나 grpc-gateway를 같이 쓰는 환경이면 함께 올려야 합니다.- NGINX가 gRPC를 HTTP/2로 처리하는 과정에서, 실제로는 업스트림이 끊기며 502로 나타나는 경우가 많습니다. 이때는 서버/클라이언트 max message가 더 흔한 병목입니다.
3) Envoy(예: Istio) 사용 시: max_grpc_message_length
서비스 메시를 쓰면 제한 지점이 하나 더 늘어납니다. Envoy는 gRPC 메시지 크기 제한을 별도로 가질 수 있고, 이를 넘으면 502/UF(Upstream Failure)로 보이기도 합니다.
Istio라면 EnvoyFilter로 조정하는 패턴이 있지만, 운영 복잡도가 커질 수 있어 가능하면 애플리케이션 레벨에서 메시지를 줄이거나 스트리밍으로 바꾸는 것이 더 안전합니다.
4) ALB Ingress Controller(EKS)에서의 관점
ALB는 NGINX처럼 client_max_body_size를 노출하지 않습니다. 대신 다음을 점검하세요.
- HTTP/2(gRPC) 리스너/타겟 그룹 구성이 올바른지
- 타겟(파드)의 응답이 중간에 끊기지 않는지(서버 max message)
- 보안 계층(WAF 등)이 바디 검사로 끊는지
만약 WAF를 붙였다면, “크기 제한”이 413이 아니라 403/502로 변형되어 보일 수 있습니다. 403이 지속될 땐 AWS WAF Bot Control 막힘으로 403 지속될 때처럼 WAF 로그에서 차단 룰/사이즈 제한을 먼저 확인하는 게 빠릅니다.
진단 체크리스트(운영에서 바로 쓰는 순서)
1) 서버가 실제로 어떤 에러를 내는지 먼저 본다
- 파드 로그에
RESOURCE_EXHAUSTED/message larger than max가 찍히면 서버 max_receive_message_length가 병목 - 서버 로그가 깨끗한데 Ingress만 502면 Ingress/프록시 병목 가능성 증가
2) 같은 요청을 “클러스터 내부에서” 직접 때려본다
Ingress를 우회하면 병목 레이어가 확 줄어듭니다.
kubectl run -it --rm grpc-debug --image=fullstorydev/grpcurl --restart=Never -- \
grpcurl -v -plaintext grpc-svc.default.svc.cluster.local:50051 list
- 내부 직격은 성공하는데 외부(Ingress 경유)만 실패하면 Ingress/ALB/WAF 쪽
- 내부 직격도 실패하면 앱/사이드카/서버 설정 쪽
3) “요청이 큰지” “응답이 큰지” 분리한다
- 요청만 크게 / 응답만 크게 각각 테스트
- 요청이 크면 서버
max_receive+ 클라이언트max_send - 응답이 크면 서버
max_send+ 클라이언트max_receive
4) 502를 본 순간, Service Endpoints도 같이 확인
가끔은 메시지 크기 문제가 아니라 엔드포인트가 0이라 502가 납니다(특히 롤링 중/라벨 꼬임). 이 경우는 완전히 다른 원인이라 먼저 배제해야 합니다. 관련 점검은 EKS에서 Pod는 뜨는데 Service Endpoints가 0일 때를 참고하세요.
“크기 제한을 올리는” 것보다 더 좋은 해법: 메시지를 줄이거나 스트리밍으로 바꾸기
운영 관점에서 gRPC 메시지 상한을 무작정 키우면:
- 프록시 메모리 사용량 증가
- tail latency 증가
- 장애 시 영향 반경 확대(큰 요청이 리트라이되면 더 치명적)
가능하면 아래를 우선 고려하세요.
- 큰 바이너리는 object storage(S3) 로 옮기고, gRPC에는 URL/키만 전달
- 대용량 응답은 server streaming으로 쪼개기
- protobuf 메시지 구조를 재설계(불필요 필드 제거, pagination)
Go 기준: 서버 스트리밍 스케치
func (s *Server) Download(req *pb.DownloadRequest, stream pb.Service_DownloadServer) error {
for chunk := range readChunks(req.Key) {
if err := stream.Send(&pb.DownloadChunk{Data: chunk}); err != nil {
return err
}
}
return nil
}
이렇게 바꾸면 “단일 메시지 최대 크기” 병목을 구조적으로 제거할 수 있습니다.
마무리: 502는 결과일 뿐, 병목은 ‘가장 낮은 max’
EKS에서 gRPC 큰 메시지가 502로 보일 때는 “ALB/Ingress가 죽었다”가 아니라, 대부분 다음 중 하나입니다.
- 서버/클라이언트 gRPC max message 불일치
- Ingress/프록시(또는 Envoy) 버퍼/제한
- WAF/보안 계층의 바디 검사/제한
해결은 경로의 모든 레이어에서 상한을 일관되게 맞추거나, 더 근본적으로는 메시지를 스트리밍/외부 스토리지로 분해하는 것입니다. 502만 보고 네트워크부터 의심하기보다, 위 체크리스트대로 “내부 직격 테스트 → 요청/응답 분리 → 가장 낮은 상한 찾기” 순서로 접근하면 재현과 해결이 훨씬 빨라집니다.