서론
고성능 서버리스 애플리케이션 제공은 Azure Functions의 핵심입니다. Azure Functions는 API, 이벤트 처리, 자동화와 같은 시나리오에서 수많은 고객 워크로드를 지원합니다. 대규모 환경에서 뛰어난 성능을 보장하기 위해, 특히 JSON 직렬화 및 역직렬화와 같은 일반적인 작업에서 대기 시간을 줄이고 처리량을 늘릴 기회를 항상 찾고 있습니다.
이를 위해, Azure Functions용 Python 워커에 빠른 JSON 라이브러리인 orjson 지원을 도입했습니다. 고객이 Function App 환경에 orjson을 추가하면, Azure Functions 런타임은 표준 json 모듈 대신 자동으로 orjson을 사용하도록 전환하여, 애플리케이션 코드 변경 없이도 상당한 성능 향상을 가져올 수 있습니다.
동기: 왜 orjson이 중요한가?
JSON 직렬화는 HTTP 페이로드 처리부터 이벤트 스트리밍에 이르기까지 많은 서버리스 워크로드의 기본입니다. HTTP를 통해 통신하거나 Event Hubs, Service Bus, Cosmos DB와 같은 이벤트 기반 서비스와 상호 작용하는 모든 Python Azure Function은 JSON 읽기 및 쓰기 작업을 포함합니다. JSON 처리는 Azure Functions Python Worker와 Azure Functions Python Library 사이에 나뉘어 있습니다. 라이브러리는 Python 객체를 JSON으로 직렬화하고 JSON 입력을 Python 객체로 역직렬화하는 역할을 하며, 워커는 단순히 원시 데이터를 함수로 주고받는 역할을 합니다. 이러한 분리는 JSON 처리가 라이브러리에 집중되어 있음을 의미하며, 모든 입력과 출력이 이러한 변환을 거쳐야 하므로 함수 성능의 핵심 요소가 됩니다.
표준 Python json 라이브러리는 안정적이지만, 높은 처리량 시나리오에 최적화되어 있지 않습니다. 페이로드 크기가 커지거나 동시성이 증가하면, JSON 직렬화는 성능 병목 현상이 되어 대기 시간과 확장성 모두에 영향을 미칠 수 있습니다. 대기 시간과 처리량을 개선하기 위해, Azure Functions Python 라이브러리는 이제 requirements.txt에 orjson이 명시되어 있거나 가상 환경에 설치되어 있으면 자동으로 orjson을 사용합니다. 이를 통해 코드 변경 없이 더 빠른 JSON 처리의 이점을 얻을 수 있습니다. orjson이 포함되어 있지 않으면, 라이브러리는 표준 json 모듈로 폴백(fallback)합니다. 고객이 더 빠른 라이브러리를 포함하는 것만으로 쉽게 성능을 개선할 수 있도록 하는 동시에, 애플리케이션을 리팩토링할 필요가 없도록 하고자 했습니다.
azure-functions
orjson
왜 orjson이 다른 JSON 라이브러리보다 뛰어난가?
Python에서 JSON 직렬화 및 역직렬화에 있어, 개발자들은 종종 내장된 json이나 simplejson, ujson과 같은 라이브러리를 사용합니다. 이러한 라이브러리들이 표준 라이브러리보다 어느 정도 개선된 성능을 제공할 수 있지만, orjson은 속도와 메모리 효율성 면에서 일관되게 이들보다 뛰어난 성능을 보입니다.
- 네이티브 Rust 구현: orjson은 Rust로 작성되어 있어, 순수 Python 또는 C 확장으로는 일반적으로 달성하기 어려운 고도로 최적화된 메모리 관리 및 성능을 가능하게 합니다.
- 더 빠른 직렬화 및 역직렬화: 벤치마크에 따르면 orjson은 크거나 깊게 중첩된 페이로드로 작업할 때 ujson보다 2~4배, json보다 최대 10배 더 빠를 수 있습니다.
- 압축된 바이너리 출력: orjson은 더 압축된 JSON 출력을 생성하여 페이로드 크기를 줄일 수 있습니다. 이는 특히 높은 처리량 또는 대역폭에 민감한 시나리오에서 큰 영향을 미칩니다.
- 엄격한 표준 준수: 일부 더 빠른 JSON 라이브러리와 달리, orjson은 기본적으로 RFC 8259를 준수하여 상호 운용성에서 발생하는 자동 오류를 방지합니다.
Azure Functions의 경우, 이는 고객이 표준 json 대비 점진적인 개선을 얻는 것을 넘어, 오늘날 Python에서 사용 가능한 가장 빠른 JSON 솔루션 중 하나를 확보하게 되어 실제 서버리스 애플리케이션에서 의미 있는 대기 시간 및 처리량 개선을 이끌어낸다는 것을 의미합니다.
성능 개선 측정
우리는 고객 시나리오에서 일반적인 메시지 크기를 반영하도록 선택된 20KB 페이로드 크기를 가진 현실적인 워크로드를 사용하여 개선 사항을 벤치마크했습니다. HTTP에는 k6를, Event Hub 시나리오에는 내부 툴링을 사용하여 로드 테스트를 수행했습니다. 페이로드 크기가 증가할수록 orjson의 이점은 더욱 두드러졌습니다. 결과는 각 테스트 케이스에 대해 세 번의 실행 중 중앙값을 취하여 측정했습니다.
로컬 테스트
표 1: HTTP 성능 - 10,000 요청 (20KB 페이로드)
| json | orjson | |
| 총 소요 시간 | 10분 01초 | 6분 01초 |
| 평균 HTTP 요청 시간 | 3.08 초 | 1.8 초 |
| 중앙값 HTTP 요청 시간 | 2.16 초 | 1.51 초 |
| 95번째 백분위수 요청 시간 | 4.66 초 | 3.52 초 |
| 드롭된 요청 수 | 289 | 0 |
| 실패한 요청 (200 외 응답) | 1.02% | 0% |
| json | orjson | |
| 총 소요 시간 | 4분 11초 | 2분 50초 |
| 초당 메시지 수 | 3.98 | 5.88 |
HTTP - 테스트 설정 및 결과
- 최대 100개 인스턴스, 2GB 메모리(기본값 유지)로 구성된 Azure Functions Flex Consumption 앱
- Azure Functions 내 부하 테스트 기능 사용 (Azure Load Testing 기반) - ALT 엔진 20개, 인스턴스당 VUH 250개.
- 테스트당 5분간의 지속적인 부하.
- 테스트된 페이로드 크기: 1KB 미만(최소), 5KB(작음), 35KB(중간), 992KB(큼) (Azure REST API 사양 파일).
|
JSON 크기 |
파서 |
오류 (%) |
평균 응답 시간 (ms) |
처리량 (req/s) |
*json* 대비 차이 (%) |
|---|---|---|---|---|---|
|
1 KB |
json |
0.00% |
780 |
10,477.78 |
– |
|
|
orjson |
0.00% |
736 |
10,632.50 | +1.48% |
|
5 KB |
json |
0.001% |
856 |
10,091.14 |
– |
|
|
orjson |
0.03% |
861 |
10,337.38 | +2.44% |
|
35 KB |
json |
0.00% |
963 |
7,305.19 |
– |
|
|
orjson |
0.00% |
933 |
7,763.25 | +6.27% |
|
992 KB |
json |
0.00% |
5,338 |
1,051.21 |
– |
|
|
orjson |
0.00% |
4,937 |
1,083.96 |
+3.12% |
Service Bus - 테스트 설정 및 결과
- 최대 100개 인스턴스, 2GB 메모리(기본값 유지)로 구성된 Azure Functions Flex Consumption 앱
- 배치 크기 100, 단일 워커를 사용하는 Service Bus LoadGen.
| SB 메시지 수 | 파서 | 총 소요 시간 (초) | 처리량 (msg/s) | *json* 대비 차이 (%) |
| 1,000 | json |
6.41 | 156 | - |
| orjson |
4.36 | 229 | +38.07% | |
| 10,000 | json |
85.40 | 117 | - |
| orjson |
72.80 | 137 | +15.92% | |
| 100,000 | json |
959.50 | 104 | - |
| orjson |
847.20 | 118 | +12.43% |
참고: JSON 작업은 배치 처리 중 Functions Python Library에서 발생합니다.
## 도전 과제 및 학습 내용 워커 수준에서 *orjson*과 같은 서드파티 라이브러리를 통합하는 데는 몇 가지 어려움이 있었습니다. * **호환성:** *orjson*이 지원되는 Python 버전(3.9 – 3.13) 전반에 걸쳐 원활하게 작동하는지 확인하기 위해 직렬화 동작의 미묘한 차이를 처리하기 위한 엄격한 테스트가 필요했습니다. * **조건부 로딩:** 런타임은 고객의 가상 환경에서 *orjson*을 사용할 수 있을 때만 동적으로 *orjson*으로 전환하도록 설계되어, *orjson*이 설치되지 않았을 때 불필요한 종속성이나 오류를 방지합니다. * **벤치마크 정확성:** 개선 사항을 정확하게 측정하려면 클라우드 환경의 일시적인 특성, 콜드 스타트, 다양한 페이로드 패턴을 고려하여 일관된 결과를 도출해야 했습니다.성공 사례
- 부하 시 지연 시간 감소: 더 큰 페이로드를 가진 워크로드에서 평균 요청 시간이 최대 40% 감소하고 드롭된 요청이 사라지는 개선 효과를 보였습니다.
- 이벤트 처리 처리량 개선: Event Hub 시나리오에서는 메시지 처리율이 거의 50% 증가하여, 애플리케이션이 버스트(bursty) 또는 대량 이벤트를 처리하는 데 도움이 되었습니다.
- 손쉬운 개선: 고객은 requirements에 orjson을 추가하는 것만으로 즉시 이점을 얻을 수 있으며, 코드 변경은 필요 없습니다.
향후 방향
- 향상된 개발자 가이드: 고객이 orjson을 효과적으로 채택하고 언제 가장 큰 이점을 얻을 수 있는지 이해할 수 있도록 모범 사례 및 권장 사항을 문서화합니다.
- 워커 심층 최적화: Python 워커에서 직렬화, 역직렬화 및 기타 성능에 중요한 경로를 개선할 수 있는 추가 기회를 모색합니다.
결론
Python 워커에서 orjson을 지원함으로써, 개발자들이 간단한 종속성 추가만으로도 상당한 성능 향상을 얻을 수 있도록 돕고 있습니다. 이러한 개선 사항은 대기 시간을 줄이고 처리량을 개선하며, Python 기반 Azure Functions 앱을 위한 더 나은 고객 경험을 제공하는 데 도움이 됩니다. 추가적인 성능 팁은 Azure Functions Python 개발자 가이드를 참조하세요. 이러한 통찰력이 종속성 선택이 서버리스 애플리케이션에서 실제 성능 개선을 이끌어낼 수 있는 방법을 탐색하는 데 영감을 주기를 바랍니다.