사용자가 티켓을 생성합니다: 모바일 앱이 느릿합니다. 대시보드를 확인합니다. CPU 사용률은 평탄합니다. 에러율은 0입니다. APM 지표는 안심할 수 있는 초록색입니다. 모든 가시적인 지표로 볼 때 백엔드는 건강합니다. 하지만 사용자가 틀린 것은 아닙니다. 느려짐은 실제이며, 안드로이드 기기와 서버 사이의 길고 어두운 통로 어딘가에서 발생하고 있습니다.

문제는 대부분의 관측성(observability) 도구가 애플리케이션 경계에서 멈춘다는 점입니다. 이 도구들은 프레임워크가 요청을 파싱한 이후에 일어나는 일들을 측정합니다. 데이터베이스 쿼리, 캐시 히트, 다운스트림 서비스 호출을 추적합니다. 하지만 이들이 놓치는 것은 요청 자체의 메커니즘입니다. OkHttp 호출이 Android HTTP 스택 내부에서 소비하는 시간, 변동성이 큰 모바일 네트워크를 통과하는 시간, TLS 핸드셰이크 협상, 그리고 코드가 실행되기도 전에 커널 큐에서 발생하는 조용한 대기 시간 등이 그것입니다. 이러한 공백은 APM이 침묵하는 동안 밀리초, 혹은 몇 초 전체를 삼켜버립니다.

암호화는 이러한 맹점을 완성시킵니다. 최신 Android 앱은 모든 것을 BoringSSL을 통해 라우팅합니다. 패킷이 커널 네트워크 스택에 도달할 때쯤이면 HTTP 헤더는 이미 암호화된 상태입니다. 표준 tcpdump나 네트워크 훅은 불투명한 TLS 레코드만 볼 수 있습니다. 트래픽이 흐르고 있다는 것은 관찰할 수 있지만, 그 내용을 읽을 수는 없습니다. 특정 커널 수준의 TCP 세그먼트를 특정 사용자의 API 호출과 연결하는 것은 확실히 불가능합니다. 필요한 트레이스 컨텍스트(trace context)가 암호문 안에 갇혀 있기 때문입니다.

eBPF는 애플리케이션 코드를 수정하지 않고도 시스템 내부에서 계측(instrument)할 수 있게 해주므로 이 방정식을 바꿉니다. 앱이 스스로 지연 시간을 보고하도록 요청하는 대신, 커널과 중요한 유저스페이스 라이브러리에 작은 프로그램을 직접 부착합니다. 이 프로그램들은 이벤트가 발생하는 즉시 관찰하고, 필요한 정보를 추출하여 링 버퍼(ring buffer)로 전송합니다. Android APK 내부에는 가벼운 헤더 외에 SDK 비대화(bloat)가 없으며, 백엔드 클래스를 재작성하는 인스트루먼테이션 에이전트도 필요하지 않습니다.

4단계 설정

이 파이프라인을 구축하려면 네 가지 별도의 관측 레이어가 필요합니다.

1. traceparent 앵커. Android 기기에서 모든 아웃바운드 요청에 W3C traceparent 헤더를 주입하는 OkHttp 인터셉터를 추가합니다. 이는 모바일 측에서 요구되는 유일한 변경 사항이며 매우 미미합니다. 이 헤더는 암호화된 페이로드 내부를 통해 백엔드까지 전달됩니다. HTTP 레이어 내부에 위치하기 때문에, TLS 엔진이 최종적으로 노출하는 평문(plaintext) 상태에서도 유지됩니다.

2. TCP 도착 타이밍. 백엔드 호스트에서는 네트워크 인터페이스에 부착된 eBPF Traffic Control 훅을 사용합니다. 이 프로그램들은 개별 TCP 세그먼트가 도착할 때마다 실행됩니다. 이들은 에지(edge)에서 시퀀스 번호와 타임스탬프를 캡처합니다. 이제 애플리케이션이 단 1바이트를 읽기도 훨씬 전에, 비트가 선로를 떠나 머신으로 들어온 정확한 시점을 알 수 있습니다.

3. 복호화 프로브. 백엔드는 OpenSSL 또는 BoringSSL을 사용하여 TLS를 종료(terminate)합니다. 여기서 아키텍처가 흥미로워집니다. 동적 유저스페이스 프로브인 uprobes를 사용하여 TLS 라이브러리 내부의 SSL_writeSSL_read에 부착합니다. 이 함수들은 평문 데이터가 암호화 엔진을 통과하는 즉시 실행됩니다. eBPF 프로그램은 해당 복호화된 버퍼를 읽어 traceparent 헤더를 스캔하고 이를 추출합니다. 이제 커널은 커스텀 도구에서 인증서나 키를 직접 다루지 않고도, 로우(raw) TCP 흐름과 특정 모바일 요청 사이의 직접적인 매핑을 보유하게 됩니다.

4. 커널 큐잉. 데이터가 복호화되어 준비된 후에도 애플리케이션이 이를 즉시 소비하지 않을 수 있습니다. 소켓 버퍼와 스케줄링 이벤트를 처리하는 관련 커널 함수에 kprobes를 부착합니다. 이를 통해 큐잉 지연 시간(queueing latency), 즉 프로세스가 CPU를 점유하기 위해 경쟁하거나 단순히 아직 read()를 호출하지 않아 커널 영역에서 요청이 대기하는 시간을 측정합니다.

네 가지 신호 소스 모두 이벤트를 eBPF 링 버퍼에 기록합니다. 유저스페이스에서 실행되는 사이드카(sidecar) 프로세스가 이 버퍼를 비우고, traceparent ID를 기준으로 이벤트를 상관 분석(correlate)하여 모든 요청에 대해 단일하고 일관된 타임라인을 재구성합니다. 이전에는 서로 연결되지 않은 파편화된 커널 노이즈에 불과했던 것이 구조화된 트레이스(trace)가 됩니다.

전체 경로 읽기

조립된 출력은 일반적으로 플레임 그래프(flame graph) 또는 지연 시간을 네 가지 구체적인 부분으로 나누는 구조화된 스팬 트리(span tree)로 렌더링됩니다.

  • Network transit time: The duration from the Android radio sending the last byte of the request to the backend NIC receiving it. This is where cellular volatility lives.
  • TLS handshake duration: The time spent negotiating the encrypted tunnel. On spotty networks, this can dwarf actual data transfer.
  • Kernel queueing latency: Time spent in kernel buffers and scheduler queues after the segment arrives but before userspace consumes it.
  • Application processing time: The slice your backend framework and business logic actually consume.

You might discover that an 800ms mobile request spends only 40ms inside your JSON parser. Another 200ms vanishes into a stalled TLS handshake across a lossy connection. Another 300ms disappears into kernel backlog on an oversubscribed host. Your APM was only reporting the 40ms. Without kernel-level visibility, you would have optimized the wrong thing entirely.

Overhead That Actually Scales

Traditional APM agents achieve their visibility by intercepting calls inside your language runtime. They wrap methods, allocate span objects, and serialize telemetry data inside your process heap. Under load, that overhead compounds quickly. Serialization costs rise. Garbage collection pressure increases. You are paying for visibility with your application's own resources.

eBPF programs run inside a kernel virtual machine and are JIT-compiled to native machine instructions. A verifier checks them for safety before they load. Each probe adds microseconds to the path, not milliseconds. The heavy work of matching events and rendering graphs happens in the sidecar, outside your service's hot path. You are not inflating heap allocations. You are not adding serialization taxes inside request handling. For services pushing thousands of requests per second, that distinction matters.

What It Takes to Run

This is not a magic bullet, and it is not a managed SaaS you can toggle on. You need a backend kernel with modern eBPF support, including BTF type information so your probes can safely traverse kernel structures. Your TLS library must expose symbols that uprobes can target; if you ship a statically linked binary with a stripped or heavily customized OpenSSL build, you will need to account for that. You also need to verify that your traceparent header survives any proxies or edge gateways between the mobile client and the TLS termination point.

But for teams exhausted by "mobile is slow" tickets that defy root-cause analysis, this architecture replaces ritual guessing with hard signal. You stop speculating about network weather and start measuring specific requests through specific pipes.

The Real Takeaway

You do not have to treat encrypted mobile traffic as an opaque stream that magically becomes visible once it hits your framework. By combining a simple OkHttp header with strategically placed eBPF probes on the backend, you can follow a single request from an Android device through TLS decryption, kernel queues, and into your application logic—without rewriting your mobile app and without instrumenting your backend code the way traditional APM demands. That is not just incremental improvement. It is end-to-end observability across