이번 글에서는 xDS 중 LDS 업데이트가 Envoy Proxy로 전파되면서 MySQL connection들이 끊기는 이슈에 대해 이야기하려 합니다.
문제상황
Istio에서 Telemetry API를 메쉬 레벨에 적용했는데, 메쉬 내에서 MySQL과 연결되어 있는 모든 컴포넌트에서 DB Connection Pool이 싹 비워지고 다시 새로운 connection으로 채워지는 이슈였습니다.
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
name: mesh-default
namespace: istio-system
spec:
metrics:
- providers:
- name: prometheus
overrides:
- match:
metric: ALL_METRICS
tagOverrides:
source_principal:
operation: REMOVE
destination_principal:
operation: REMOVE
spring-boot-jib 2024-10-26T08:10:21.982Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@49ced9c7 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.984Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@39815baa (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.985Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@8fbefc6 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.986Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@4862d015 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.987Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@4b1b657b (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.987Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@78743bb9 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.988Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@2fa66eae (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.989Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@711af137 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.990Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@61ba72d2 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring-boot-jib 2024-10-26T08:10:21.990Z WARN 1 --- [spring-demo] [nio-8080-exec-2] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@38ca34af (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
DB에 query 또는 command를 처리하고 있는 상황이 아니면 해당 로그는 발생하지 않았고, 요청의 처리속도가 조금 느려지는 선에서 끝났습니다. 따라서 파악한 흐름에 따르면 아래와 같았습니다:
DB 요청 -> 도중에 connection유실 -> 풀의 다른 connection을 사용하려 했으나 모두 실패해서 전부 풀에서 제거 -> connection을 새로 생성해서 풀을 다시 채움 -> 정상적 요청 처리
제 상황에서는 DB의 connection을 새로 연결하는 과정이 요청의 실패로 이어지지는 않았습니다. query/command 모두 정상적으로 처리되어 예상된 만큼의 레코드가 DB에 존재하고 있었습니다. 하지만 풀의 사이즈가 더 크고, 레이턴시가 길다면 timeout 실패로 이어질 가능성이 있고, MSA 환경에서는 에러가 퍼질 가능성이 보였습니다.
Istio?
Istio는 K8s/VM 환경에 Service Mesh를 제공하는 오픈소스 프로젝트입니다.
파드당, 또는 노드당 하나의 L7 proxy를 붙여서 iptables로 해당 워크로드의 트래픽을 proxy를 무조건 선행해서 거치도록 조정해서 data plane을 설정하고, control plane에서 proxy로 L7 라우팅 정보를 삽입해서 워크로드들의 트래픽을 통제할 수 있습니다.
Envoy?
Lyft라고 하는 모빌리티 서비스 회사 (Uber와 유사)에서 C++로 작성한 L7 proxy이자 웹 서버입니다. 외부에 xDS라는 API를 제공해서 이 프로토콜을 통해 내부의 구성정보(listener, route, cluster, endpoint 등)을 업데이트할 수 있습니다.
일반적으로 Istio에서 자주 변경되는 VirtualService, ServieEntry Custom Resource의 적용 과정은 Cluster, Route, Endpoint 등의 설정에 영향을 미칩니다.
제가 이번에 삽입한 Telemetry API는 전체 메쉬에 영향을 주며, 메트릭에서 특정 label을 제거하는 설정이었습니다.
func generateStatsConfig(class networking.ListenerClass, filterConfig telemetryFilterConfig) *anypb.Any {
if !filterConfig.Metrics {
// No metric for prometheus
return nil
}
listenerCfg := filterConfig.MetricsForClass(class)
if listenerCfg.Disabled {
// no metrics for this listener
return nil
}
cfg := stats.PluginConfig{
DisableHostHeaderFallback: disableHostHeaderFallback(class),
TcpReportingDuration: filterConfig.ReportingInterval,
RotationInterval: filterConfig.RotationInterval,
GracefulDeletionInterval: filterConfig.GracefulDeletionInterval,
}
for _, override := range listenerCfg.Overrides {
metricName, f := metricToPrometheusMetric[override.Name]
if !f {
// Not a predefined metric, must be a custom one
metricName = override.Name
}
mc := &stats.MetricConfig{
Dimensions: map[string]string{},
Name: metricName,
Drop: override.Disabled,
}
for _, t := range override.Tags {
if t.Remove {
mc.TagsToRemove = append(mc.TagsToRemove, t.Name)
} else {
mc.Dimensions[t.Name] = t.Value
}
}
cfg.Metrics = append(cfg.Metrics, mc)
}
return protoconv.MessageToAny(&cfg)
}
하지만 해당 메트릭들은 listener들이 만들던 통계치를 통해 제공하던 정보였으며, 이를 제거하는 것이 LDS로 전파되면서 connection들이 drain되었습니다.
// ServiceEntry 적용, 이전과 LDS resource 개수 및 크기가 동일
2024-10-26T09:03:53.888825Z info delta CDS: PUSH for node:spring-b4dcbcf76-m4wwb.test resources:8 removed:0 size:3.2kB cached:4/4
2024-10-26T09:03:53.904528Z info delta LDS: PUSH for node:spring-b4dcbcf76-m4wwb.test resources:34 removed:0 size:109.0kB
2024-10-26T09:03:53.922663Z info delta RDS: PUSH for node:spring-b4dcbcf76-m4wwb.test resources:17 removed:0 size:19.4kB cached:17/17
// Telemetry 적용, LDS resources 크기가 이전보다 증가 (109kb -> 153kb)
2024-10-26T09:04:04.210051Z info delta LDS: PUSH for node:spring-b4dcbcf76-m4wwb.test resources:34 removed:0 size:153.2kB
이 과정에서 draining이 될때 HTTP, Redis 같이 Connection Manager가 별도로 있는 경우에는 graceful하게 drain이 됐지만, MySQL의 경우 그렇지 못해 connection이 끊기고 연결되는 과정이 매끄럽지 못했습니다.
결론
결과적으로 MySQL은 envoy에서 지원하는 connection manager가 없어 connection의 관리가 힘들었습니다. 그래서 결국 해당 포트가 iptables로 proxy로 타지 않게 설정하는 excludeEgressPorts 설정으로 포트를 제외해 커넥션들이 서비스 컨테이너에서 DB로 직결하여 proxy LDS로 인한 draining에 영향을 받지 않도록 설정했습니다.
일반적인 ServiceEntry, 파드 변경 등으로 proxy가 설정을 업데이트할때 seamless하게 처리되어서 안일하게 생각한 면이 있었습니다. envoy에 설정이 인입될때, xDS의 어떤 부분이 변경되는지 유의할 필요가 있고, 만약 LDS일 경우에는 envoy에서 지원하지 않는 프로토콜을 사용하는데 proxy를 거치는지 확인할 필요가 있다는걸 배운 경험이었습니다.