1. Triển khai OpenTelemetry Collector dưới dạng DaemonSet
Chúng ta sẽ triển khai OpenTelemetry Collector (OTel Collector) dưới dạng DaemonSet để đảm bảo mỗi node trong cụm Kubernetes đều có một agent thu thập trace.
Việc sử dụng DaemonSet giúp collector lắng nghe trên port local (thường là 4317 cho gRPC hoặc 4318 cho HTTP) để các service trên cùng node có thể gửi dữ liệu vào mà không cần cấu hình mạng phức tạp giữa các namespace.
Tạo file cấu hình DaemonSet với đường dẫn đầy đủ là ./manifests/otel-collector-daemonset.yaml.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector
namespace: observability
labels:
app: otel-collector
spec:
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
serviceAccountName: otel-collector
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.95.0
args:
- --config=/etc/otelcol-contrib/config.yaml
ports:
- containerPort: 4317
name: otlp-grpc
- containerPort: 4318
name: otlp-http
- containerPort: 55680
name: jaeger-grpc
volumeMounts:
- name: config
mountPath: /etc/otelcol-contrib/config.yaml
subPath: config.yaml
- name: host-logs
mountPath: /var/log
readOnly: true
resources:
limits:
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
env:
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
volumes:
- name: config
configMap:
name: otel-collector-config
- name: host-logs
hostPath:
path: /var/log
type: Directory
File DaemonSet đã được tạo. Bước tiếp theo là tạo ConfigMap chứa cấu hình logic của collector để DaemonSet có thể mount vào.
2. Cấu hình Receiver và Exporter cho OpenTelemetry Collector
Bây giờ chúng ta cần định nghĩa cách collector nhận dữ liệu (Receiver) và gửi đi đâu (Exporter).
Trong phần này, chúng ta sẽ cấu hình Receiver để lắng nghe OTLP (gRPC/HTTP) và Exporter để gửi trace vào Jaeger (backend mặc định cho demo) hoặc Tempo (cho production Grafana).
Tạo file ConfigMap với đường dẫn ./manifests/otel-collector-config.yaml.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: observability
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:55680
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
sampling:
parent_based:
default:
with_trace_sampled: 100
with_trace_unsampled: 10
exporters:
otlp/jaeger:
endpoint: jaeger-collector.observability.svc.cluster.local:4317
tls:
insecure: true
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp, jaeger]
processors: [memory_limiter, sampling, batch]
exporters: [otlp/jaeger, debug]
Cấu hình trên thiết lập pipeline trace: Nhận từ OTLP/Jaeger -> Xử lý giới hạn bộ nhớ (memory_limiter) -> Lấy mẫu (sampling) -> Gộp gói (batch) -> Xuất vào Jaeger và Debug exporter.
Lưu ý: Endpoint jaeger-collector.observability.svc.cluster.local giả định bạn đã deploy Jaeger trong namespace observability ở các phần trước.
Triển khai cả DaemonSet và ConfigMap bằng lệnh:
kubectl apply -f ./manifests/otel-collector-config.yaml
kubectl apply -f ./manifests/otel-collector-daemonset.yaml
Verify kết quả: Kiểm tra trạng thái pod và logs để đảm bảo collector khởi động thành công.
kubectl get pods -n observability -l app=otel-collector
kubectl logs -n observability -l app=otel-collector --tail=50
Kết quả mong đợi: Pod ở trạng thái Running và logs xuất hiện dòng Service started cùng với thông báo Exporter otlp/jaeger connected.
3. Tích hợp OpenTelemetry SDK vào ứng dụng mẫu (Go)
Sau khi backend collector đã chạy, chúng ta cần instrument (chèn code) vào ứng dụng để tạo trace context.
Chúng ta sẽ sử dụng thư viện OpenTelemetry cho Go. Ứng dụng sẽ tạo một span mẫu và gửi đến collector đã deploy ở bước 1.
Tạo file main.go trong thư mục ứng dụng của bạn.
package main
import (
"context"
"log"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"google.golang.org/grpc"
)
func main() {
// Lấy địa chỉ collector từ môi trường hoặc mặc định
collectorAddr := os.Getenv("OTEL_COLLECTOR_ENDPOINT")
if collectorAddr == "" {
// Trong K8s, thường dùng DNS service hoặc IP node nếu dùng DaemonSet local
// Ở đây giả định dùng DNS service cho demo đơn giản
collectorAddr = "otel-collector.observability.svc.cluster.local:4317"
}
// Tạo gRPC client kết nối tới collector
conn, err := grpc.Dial(collectorAddr, grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to create gRPC connection: %v", err)
}
defer conn.Close()
// Cấu hình exporter
exporter, err := otlptracegrpc.New(
context.Background(),
otlptracegrpc.WithGRPCConn(conn),
)
if err != nil {
log.Fatalf("Failed to create exporter: %v", err)
}
// Cấu hình resource (thông tin về service)
res, err := resource.New(
context.Background(),
resource.WithAttributes(
semconv.ServiceName("sample-go-service"),
semconv.ServiceVersion("1.0.0"),
),
)
if err != nil {
log.Fatalf("Failed to create resource: %v", err)
}
// Tạo TracerProvider với sampler và exporter
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysOn()),
)
defer tp.Shutdown(context.Background())
otel.SetTracerProvider(tp)
// Tạo context và bắt đầu trace
ctx := context.Background()
tracer := otel.Tracer("sample-go-service")
// Tạo một span mẫu
ctx, span := tracer.Start(ctx, "process-request")
defer span.End()
log.Println("Starting process request...")
time.Sleep(100 * time.Millisecond) // Giả lập xử lý
span.SetAttributes(semconv.HTTPStatusCode(200))
log.Println("Request processed successfully.")
// Simulate một span con (child span)
ctx, subSpan := tracer.Start(ctx, "call-database")
defer subSpan.End()
time.Sleep(50 * time.Millisecond)
log.Println("Database query executed.")
}
Code trên tạo một TracerProvider, kết nối tới OTel Collector qua gRPC, và thực hiện một chuỗi request với span cha và span con.
Để chạy thử nghiệm, build image và deploy vào K8s:
docker build -t my-app-go:latest .
kubectl create -f -
Verify kết quả: Xem logs của pod để đảm bảo không có lỗi kết nối, sau đó kiểm tra dashboard Jaeger.
kubectl logs -n observability sample-go-pod
Kết quả mong đợi: Logs xuất hiện dòng "Starting process request..." và không có lỗi connection refused. Truy cập Jaeger UI (thường port 16686) sẽ thấy trace mới có tên "sample-go-service".
4. Cấu hình Sampling Strategy để cân bằng hiệu năng
Trong môi trường Hybrid Cloud với lượng request lớn, việc ghi lại 100% trace sẽ gây quá tải cho storage và mạng.
Chúng ta cần cấu hình Sampling Strategy trong OpenTelemetry Collector để chỉ giữ lại một tỷ lệ trace nhất định hoặc dựa trên xác suất.
Chỉnh sửa file ConfigMap ./manifests/otel-collector-config.yaml (cụ thể là phần processors.sampling) để áp dụng chiến lược Parent-Based Sampling.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: observability
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 400
# Cấu hình Parent-Based Sampling
sampling:
parent_based:
default:
# Nếu trace parent là sampled, giữ 100% (để đảm bảo tính toàn vẹn của trace lỗi)
with_trace_sampled: 100
# Nếu trace parent là unsampled, lấy mẫu 10% (để giảm tải)
with_trace_unsampled: 10
# Hoặc dùng probabilistic nếu không muốn phụ thuộc vào parent
# probabilistic:
# sampling_percentage: 5
exporters:
otlp/jaeger:
endpoint: jaeger-collector.observability.svc.cluster.local:4317
tls:
insecure: true
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, sampling, batch]
exporters: [otlp/jaeger, debug]
Chiến lược Parent-Based ở trên rất quan trọng: nếu một trace đã được đánh dấu là "lỗi" (thường được force sample ở ứng dụng), toàn bộ trace đó sẽ được giữ lại (100%) dù tỷ lệ lấy mẫu chung là thấp. Điều này đảm bảo debugging các lỗi vẫn chính xác.
Áp dụng lại cấu hình mới:
kubectl apply -f ./manifests/otel-collector-config.yaml
kubectl rollout restart daemonset otel-collector -n observability
Verify kết quả: Chạy nhiều lần pod ứng dụng Go (hoặc dùng kubectl run để tạo load) và quan sát số lượng trace xuất hiện trong Jaeger.
kubectl run -n observability load-test --image=my-app-go:latest --restart=Never
kubectl delete pod -n observability load-test
# Lặp lại lệnh trên 10 lần
Kết quả mong đợi: Nếu bạn chạy 10 lần nhưng cấu hình sampling là 10% (với trace mới), bạn sẽ thấy khoảng 1-2 trace xuất hiện trong Jaeger (do tính ngẫu nhiên). Nếu bạn tạo trace có lỗi (force sample), tỷ lệ xuất hiện sẽ là 100%.
Điều hướng series:
Mục lục: Series: Series: Xây dựng hệ thống observability toàn diện (Logging, Metrics, Tracing) cho môi trường Hybrid Cloud với Prometheus, Grafana và OpenTelemetry
« Phần 3: Triển khai nền tảng Logging: Xây dựng pipeline tập trung với Loki
Phần 5: Tích hợp và trực quan hóa dữ liệu: Xây dựng Dashboard với Grafana »