안녕하세요 여러분! 저는 MY.GAMES의 Java 개발자인 Dmitriy Apanasevich입니다. Rush Royale 게임을 작업하고 있으며, OpenTelemetry 프레임워크를 Java 백엔드에 통합한 경험을 공유하고 싶습니다. 여기서 다룰 내용이 꽤 많습니다. 이를 구현하는 데 필요한 코드 변경 사항과 설치 및 구성해야 했던 새 구성 요소를 다루고, 물론 결과 중 일부를 공유하겠습니다.
우리의 사례에 대해 좀 더 맥락을 제공하겠습니다. 개발자로서 우리는 모니터링, 평가 및 이해하기 쉬운 소프트웨어를 만들고 싶습니다(그리고 이것이 바로 OpenTelemetry를 구현하는 목적입니다. 시스템 성능을 극대화하기 위해
애플리케이션 성능에 대한 통찰력을 수집하기 위한 기존 방법에는 종종 이벤트, 메트릭 및 오류를 수동으로 로깅하는 것이 포함됩니다.
물론, 로그를 처리할 수 있는 프레임워크는 다양하며, 이 글을 읽는 모든 분들은 로그를 수집, 저장, 분석하기 위한 시스템을 갖추고 있을 것으로 확신합니다.
로깅도 완전히 구성되었으므로 로그 작업을 위해 OpenTelemetry가 제공하는 기능을 사용하지 않았습니다.
시스템을 모니터링하는 또 다른 일반적인 방법은 메트릭을 활용하는 것입니다.
또한 우리는 지표를 수집하고 시각화하기 위한 완전히 구성된 시스템을 갖고 있었으므로 여기서도 지표 작업 측면에서 OpenTelemetry의 기능을 무시했습니다.
그러나 이러한 종류의 시스템 데이터를 얻고 분석하는 데 덜 일반적인 도구는 다음과 같습니다.
추적은 요청이 수명 동안 시스템을 통과하는 경로를 나타내며 일반적으로 시스템이 요청을 수신할 때 시작하여 응답으로 끝납니다. 추적은 여러 가지로 구성됩니다.
이 논의에서는 OpenTelemetry의 추적 측면에 집중하겠습니다.
또한 OpenTelemetry 프로젝트에 대해서도 살펴보겠습니다. 이 프로젝트는 다음 두 가지를 병합하여 만들어졌습니다.
OpenTelemetry는 이제 다양한 프로그래밍 언어에 대한 API, SDK, 도구 세트를 정의하는 표준을 기반으로 포괄적인 범위의 구성 요소를 제공하며, 이 프로젝트의 주요 목표는 데이터를 생성, 수집, 관리, 내보내는 것입니다.
즉, OpenTelemetry는 데이터 저장이나 시각화 도구를 위한 백엔드를 제공하지 않습니다.
우리는 추적에만 관심이 있었기 때문에 추적을 저장하고 시각화하기 위한 가장 인기 있는 오픈 소스 솔루션을 살펴보았습니다.
궁극적으로 우리는 인상적인 시각화 기능, 빠른 개발 속도, 메트릭 시각화를 위한 기존 Grafana 설정과의 통합으로 인해 Grafana Tempo를 선택했습니다. 단일 통합 도구를 갖는 것도 상당한 이점이었습니다.
OpenTelemetry의 구성 요소에 대해서도 간략히 살펴보겠습니다.
사양:
API - 데이터 유형, 작업, 열거형
SDK — 사양 구현, 다양한 프로그래밍 언어의 API. 언어가 다르면 알파에서 안정까지 다른 SDK 상태가 됩니다.
데이터 프로토콜(OTLP) 및
Java API SDK:
OpenTelemetry Collector는 데이터를 수신하고 처리하고 전달하는 프록시로서 중요한 구성 요소입니다. 자세히 살펴보겠습니다.
초당 수천 개의 요청을 처리하는 고부하 시스템의 경우 데이터 볼륨을 관리하는 것이 중요합니다. 추적 데이터는 종종 볼륨 면에서 비즈니스 데이터를 능가하므로 수집하고 저장할 데이터의 우선순위를 정하는 것이 필수적입니다. 여기서 데이터 처리 및 필터링 도구가 등장하여 어떤 데이터를 저장할 가치가 있는지 결정할 수 있습니다. 일반적으로 팀은 다음과 같은 특정 기준을 충족하는 추적을 저장하려고 합니다.
어떤 추적 정보를 저장하고 어떤 정보를 삭제할지 결정하는 데 사용되는 두 가지 주요 샘플링 방법은 다음과 같습니다.
OpenTelemetry Collector는 데이터 수집 시스템을 구성하여 필요한 데이터만 저장하도록 돕습니다. 나중에 구성에 대해 논의하겠지만, 지금은 추적을 생성하기 위해 코드에서 무엇을 변경해야 하는지에 대한 질문으로 넘어가겠습니다.
추적 생성을 위해서는 최소한의 코딩만 필요했습니다. Java 에이전트로 애플리케이션을 시작하고 다음을 지정하기만 하면 됩니다.
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry는 엄청난 수의
에이전트 구성에서 추적에서 보고 싶지 않은 스팬이 있는 라이브러리를 비활성화하고 코드가 작동하는 방식에 대한 데이터를 얻기 위해 이를 표시했습니다.
@WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); }
이 예제에서 @WithSpan
주석은 메서드에 사용되었으며, 이는 " acquire locks
"라는 이름의 새 span을 생성해야 한다는 신호를 보내고, " locks
" 속성은 메서드 본문에서 생성된 span에 추가됩니다.
메서드가 작업을 마치면 span이 닫히고 비동기 코드의 경우 이 세부 사항에 주의를 기울이는 것이 중요합니다. 주석이 달린 메서드에서 호출된 람다 함수에서 비동기 코드의 작업과 관련된 데이터를 가져와야 하는 경우 이러한 람다를 별도의 메서드로 분리하고 추가 주석으로 표시해야 합니다.
이제 전체 추적 수집 시스템을 구성하는 방법에 대해 이야기해 보겠습니다. 모든 JVM 애플리케이션은 OpenTelemetry 수집기에 데이터를 보내는 Java 에이전트로 시작됩니다.
그러나 단일 수집기는 대량의 데이터 흐름을 처리할 수 없으며 시스템의 이 부분은 확장되어야 합니다. 각 JVM 애플리케이션에 대해 별도의 수집기를 시작하면 꼬리 샘플링이 중단됩니다. 추적 분석은 하나의 수집기에서 이루어져야 하며 요청이 여러 JVM을 거치는 경우 하나의 추적 범위가 다른 수집기에 도달하여 분석이 불가능하기 때문입니다.
여기서,
결과적으로, 우리는 다음과 같은 시스템을 얻습니다. 각 JVM 애플리케이션은 동일한 밸런서 컬렉터로 데이터를 전송하고, 이 컬렉터의 유일한 작업은 다른 애플리케이션에서 수신한 데이터를 주어진 트레이스와 관련된 동일한 컬렉터-프로세서로 분배하는 것입니다. 그런 다음 컬렉터-프로세서는 데이터를 Grafana Tempo로 전송합니다.
이 시스템의 구성 요소의 구성을 자세히 살펴보겠습니다.
수집기-밸런서 구성에서는 다음과 같은 주요 부분을 구성했습니다.
receivers: otlp: protocols: grpc: exporters: loadbalancing: protocol: otlp: tls: insecure: true resolver: static: hostnames: - collector-1.example.com:4317 - collector-2.example.com:4317 - collector-3.example.com:4317 service: pipelines: traces: receivers: [otlp] exporters: [loadbalancing]
수집기-프로세서의 구성은 더 복잡하므로 이를 살펴보겠습니다.
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:14317 processors: tail_sampling: decision_wait: 10s num_traces: 100 expected_new_traces_per_sec: 10 policies: [ { name: latency500-policy, type: latency, latency: {threshold_ms: 500} }, { name: error-policy, type: string_attribute, string_attribute: {key: error, values: [true, True]} }, { name: probabilistic10-policy, type: probabilistic, probabilistic: {sampling_percentage: 10} } ] resource/delete: attributes: - key: process.command_line action: delete - key: process.executable.path action: delete - key: process.pid action: delete - key: process.runtime.description action: delete - key: process.runtime.name action: delete - key: process.runtime.version action: delete exporters: otlp: endpoint: tempo:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [otlp]
컬렉터-밸런서 구성과 유사하게 처리 구성은 수신기, 내보내기, 서비스 섹션으로 구성됩니다. 그러나 데이터가 처리되는 방식을 설명하는 프로세서 섹션에 초점을 맞출 것입니다.
첫째, tail_sampling 섹션은 다음을 보여줍니다.
delay500-policy : 이 규칙은 지연 시간이 500밀리초를 초과하는 추적을 선택합니다.
error-policy : 이 규칙은 처리 중에 오류가 발생한 추적을 선택합니다. 추적 범위에서 "true" 또는 "True" 값을 가진 "error"라는 문자열 속성을 검색합니다.
probabilistic10-policy : 이 규칙은 모든 추적의 10%를 무작위로 선택하여 일반적인 애플리케이션 작동, 오류 및 긴 요청 처리에 대한 통찰력을 제공합니다.
이 예제에서는 tail_sampling 외에도 데이터 분석 및 저장에 필요하지 않은 속성을 삭제하는 resource/delete 섹션을 보여줍니다.
그 결과 Grafana 추적 검색 창을 사용하면 다양한 기준으로 데이터를 필터링할 수 있습니다. 이 예에서는 게임 메타데이터를 처리하는 로비 서비스에서 수신한 추적 목록을 간단히 표시합니다. 이 구성을 사용하면 지연, 오류 및 무작위 샘플링과 같은 속성으로 나중에 필터링할 수 있습니다.
추적 보기 창에는 요청을 구성하는 다양한 스팬을 포함하여 로비 서비스의 실행 타임라인이 표시됩니다.
그림에서 볼 수 있듯이 이벤트 순서는 다음과 같습니다. 잠금이 획득된 후 캐시에서 객체가 검색되고, 요청을 처리하는 트랜잭션이 실행된 후 객체가 다시 캐시에 저장되고 잠금이 해제됩니다.
데이터베이스 요청과 관련된 스팬은 표준 라이브러리의 계측으로 인해 자동으로 생성되었습니다. 반면, 잠금 관리, 캐시 작업 및 트랜잭션 시작과 관련된 스팬은 앞서 언급한 주석을 사용하여 비즈니스 코드에 수동으로 추가되었습니다.
스팬을 볼 때, 처리 중에 발생한 일을 더 잘 이해할 수 있게 해주는 속성을 볼 수 있습니다. 예를 들어, 데이터베이스의 쿼리를 볼 수 있습니다.
Grafana Tempo의 흥미로운 기능 중 하나는 다음과 같습니다.
우리가 보았듯이, OpenTelemetry 추적을 사용하면 관찰 능력이 꽤 훌륭하게 향상되었습니다. 최소한의 코드 변경과 잘 구성된 수집기 설정으로 심층적인 통찰력을 얻었고, Grafana Tempo의 시각화 기능이 어떻게 우리의 설정을 더욱 보완하는지 보았습니다. 읽어주셔서 감사합니다!