GKE 容器被 'Memory cgroup out of memory' 终止,但监控、本地测试和 pprof 显示使用率远低于限制

GKE container killed by 'Memory cgroup out of memory' but monitoring, local testing and pprof shows usage far below limit

我最近将一个新的容器映像推送到我的 GKE 部署之一,并注意到 API 延迟上升并且请求开始返回 502。

查看日志我发现容器因为 OOM 而开始崩溃:

Memory cgroup out of memory: Killed process 2774370 (main) total-vm:1801348kB, anon-rss:1043688kB, file-rss:12884kB, shmem-rss:0kB, UID:0 pgtables:2236kB oom_score_adj:980

查看内存使用图,pods 似乎使用的内存加起来并不超过 50MB。我最初的资源请求是:

...
spec:
...
  template:
...
    spec:
...
      containers:
      - name: api-server
...
        resources:
          # You must specify requests for CPU to autoscale
          # based on CPU utilization
          requests:
            cpu: "150m"
            memory: "80Mi"
          limits:
            cpu: "1"
            memory: "1024Mi"
      - name: cloud-sql-proxy
        # It is recommended to use the latest version of the Cloud SQL proxy
        # Make sure to update on a regular schedule!
        image: gcr.io/cloudsql-docker/gce-proxy:1.17
        resources:
          # You must specify requests for CPU to autoscale
          # based on CPU utilization
          requests:
            cpu: "100m"
...

然后我尝试将 API 服务器的请求增加到 1GB,但没有帮助。最后,有用的是将容器镜像恢复到以前的版本:

查看golang二进制文件的变化,没有明显的内存泄漏。当我 运行 它在本地使用最多 80MB 的内存,即使在来自与生产中相同的请求的负载下也是如此。

我从 GKE 控制台获得的上图也显示了 pod 使用的内存远远少于 1GB 内存限制。

所以我的问题是:当 GKE 监控和 运行在本地仅使用 1GB 限制中的 80MB 时,什么会导致 GKE 因 OOM 终止我的进程?

=== 编辑 ===

添加同一中断的另一个图表。这次拆分 pod 中的两个容器。如果我没理解错的话,这里的指标是non-evictable container/memory/used_bytes:

container/memory/used_bytes GA
Memory usage
GAUGE, INT64, By
k8s_container   Memory usage in bytes. Sampled every 60 seconds.
memory_type: Either `evictable` or `non-evictable`. Evictable memory is memory that can be easily reclaimed by the kernel, while non-evictable memory cannot.

编辑 2021 年 4 月 26 日

我尝试将部署 yaml 中的资源字段更新为请求的 1GB RAM 和 1GB RAM 限制,如 Paul 和 Ryan 所建议的:

        resources:
          # You must specify requests for CPU to autoscale
          # based on CPU utilization
          requests:
            cpu: "150m"
            memory: "1024Mi"
          limits:
            cpu: "1"
            memory: "1024Mi"

不幸的是,用 kubectl apply -f api_server_deployment.yaml:

更新后结果相同
{
 insertId: "yyq7u3g2sy7f00"  
 jsonPayload: {
  apiVersion: "v1"   
  eventTime: null   
  involvedObject: {
   kind: "Node"    
   name: "gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy"    
   uid: "gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy"    
  }
  kind: "Event"   
  message: "Memory cgroup out of memory: Killed process 1707107 (main) total-vm:1801412kB, anon-rss:1043284kB, file-rss:9732kB, shmem-rss:0kB, UID:0 pgtables:2224kB oom_score_adj:741"   
  metadata: {
   creationTimestamp: "2021-04-26T23:13:13Z"    
   managedFields: [
    0: {
     apiVersion: "v1"      
     fieldsType: "FieldsV1"      
     fieldsV1: {
      f:count: {
      }
      f:firstTimestamp: {
      }
      f:involvedObject: {
       f:kind: {
       }
       f:name: {
       }
       f:uid: {
       }
      }
      f:lastTimestamp: {
      }
      f:message: {
      }
      f:reason: {
      }
      f:source: {
       f:component: {
       }
       f:host: {
       }
      }
      f:type: {
      }
     }
     manager: "node-problem-detector"      
     operation: "Update"      
     time: "2021-04-26T23:13:13Z"      
    }
   ]
   name: "gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy.16798b61e3b76ec7"    
   namespace: "default"    
   resourceVersion: "156359"    
   selfLink: "/api/v1/namespaces/default/events/gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy.16798b61e3b76ec7"    
   uid: "da2ad319-3f86-4ec7-8467-e7523c9eff1c"    
  }
  reason: "OOMKilling"   
  reportingComponent: ""   
  reportingInstance: ""   
  source: {
   component: "kernel-monitor"    
   host: "gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy"    
  }
  type: "Warning"   
 }
 logName: "projects/questions-279902/logs/events"  
 receiveTimestamp: "2021-04-26T23:13:16.918764734Z"  
 resource: {
  labels: {
   cluster_name: "api-us-central-1"    
   location: "us-central1-a"    
   node_name: "gke-api-us-central-1-e2-highcpu-4-nod-dfe5c3a6-c0jy"    
   project_id: "questions-279902"    
  }
  type: "k8s_node"   
 }
 severity: "WARNING"  
 timestamp: "2021-04-26T23:13:13Z"  
}

Kubernetes 似乎几乎立即杀死了使用 1GB 内存的容器。但同样,指标显示容器仅使用 2MB 内存:

我再次感到难过,因为即使在负载下,当我在本地 运行 这个二进制文件使用的空间也不超过 80MB。

我也试过运行宁go tool pprof <url>/debug/pprof/heap。它显示了几个不同的值,因为 Kubernetes 不断地破坏容器。但是 none 高于 ~20MB 并且内存使用量没有异常

编辑 04/27

我尝试为 pod 中的两个容器设置 request=limit:

 requests:
   cpu: "1"
   memory: "1024Mi"
 limits:
   cpu: "1"
   memory: "1024Mi"
...
requests:
  cpu: "100m"
  memory: "200Mi"
limits:
  cpu: "100m"
  memory: "200Mi"

但是也没用:

Memory cgroup out of memory: Killed process 2662217 (main) total-vm:1800900kB, anon-rss:1042888kB, file-rss:10384kB, shmem-rss:0kB, UID:0 pgtables:2224kB oom_score_adj:-998

并且内存指标仍以个位数 MB 显示使用情况。

04/30 更新

我煞费苦心地逐一检查我的最新提交,从而查明了似乎导致此问题的更改。

在有问题的提交中,我有几行像

type Pic struct {
        image.Image
        Proto *pb.Image
}
...

pic.Image = picture.Resize(pic, sz.Height, sz.Width)
...

其中 picture.Resize 最终调用 resize.Resize。 我将其更改为:

type Pic struct {
        Img   image.Image
        Proto *pb.Image
 }
...
pic.Img = picture.Resize(pic.Img, sz.Height, sz.Width)

这解决了我眼前的问题,现在容器 运行 没问题了。但它没有回答我原来的问题:

  1. 为什么这些行会导致 GKE OOM 我的容器?
  2. 为什么 GKE 内存指标显示一切正常?

我猜是Pod QoS class

造成的

当系统过载时,QoS classes 确定首先杀死哪个 pod,以便将释放的资源分配给更高优先级 pods。

在您的情况下,您的 pod 的 QoS 将是 Burstable

每个 运行 进程都有一个 OutOfMemory(OOM) 分数。系统通过比较所有 运行 个进程的 OOM 分数来选择要杀死的进程。当需要释放内存时,得分最高的进程将被杀死。 score的计算方法请参考How is kernel oom score calculated?.

如果都在Burstableclass中,哪个pod先被杀死?

简而言之,系统将杀死一个使用比另一个更多的请求内存的百分比。

Pod A

used: 90m
requests: 100m
limits: 200m
Pod B

used: 150m
requests: 200m
limits: 400m

Pod A 将在 Pod B 之前被杀死,因为它使用了其请求内存的 90%,而 Pod B 仅使用其请求内存的 75%。

此处的资源规范是 OOM 的根本原因。

在 Kubernetes 中,必需内存和有限内存的定义不同。所需内存为内存must-have。 limited memory 是容器可以爆破的内存。但是有限的内存并不能保证容器可以拥有那个资源。

在大多数生产系统中,不建议限制资源和所需资源相差太大。例如,在您的情况下,

requests:
  cpu: "150m"
  memory: "80Mi"
limits:
  cpu: "1"
  memory: "1024Mi"

容器只能有80Mi保证内存,但它可以莫名其妙地爆到1024Mi。节点可能没有足够的内存给容器,容器本身就会 OOM。

所以,如果你想改善这种情况,你需要将资源配置成这样。

requests:
  cpu: "150m"
  memory: "1024Mi"
limits:
  cpu: "1"
  memory: "1024Mi"

请注意 CPU 就可以了,因为您不会在低 CPU 时间下终止进程。但是OOM会导致进程kill掉。

正如上面的回答所说,这与pod中的服务质量有关。一般来说,对于大多数最终用户,您应该始终将您的容器配置为保证 class,即请求 == 限制。在将其配置为突发 class.

之前,您可能需要一些理由

确保 QoS class 为“保证”对您的情况没有帮助。您的一个进程导致父 cgroup 超出其内存限制 - 反过来由您针对相应容器指定的内存限制值设置 - OOM 杀手终止它。这不是 Pod 驱逐,因为您在日志中清楚地看到 OOM 杀手的商标信息。 “保证”QoS class 的场景会有所帮助,如果另一个 pod 分配了太多内存,使节点处于内存压力之下——在这种情况下,您的“保证”pod 将被保留。但是在你的情况下,Kubelet 从来没有在这一切中得到任何消息——就像决定完全驱逐 pod 一样——因为 OOM killer acts faster.

Burak Serdar 在其评论中有一个很好的观点 - 大内存块的临时分配。情况很可能是这样,因为在您的情况下,从您粘贴的消息中收集数据的分辨率是 60 秒。那是很多时间。可以在不到 1 秒的时间内轻松填满 GB 的 RAM。我的假设是,内存“尖峰”永远不会呈现,因为指标永远不会及时收集(即使您直接查询 cAdvisor 也会很棘手,因为它收集指标的分辨率为 10-15 秒)。

如何更多地了解正在发生的事情?一些想法:

  • 有一些工具可以显示应用程序实际分配了多少,一直到框架级别。在 .NET 中,dotMemory 是一种常用工具,它可以 运行 在容器内并捕获正在发生的事情。 Go 可能有一个等价物。这种方法的问题在于,当容器被 OOMKilled 时,该工具也会随之关闭
  • 在您自己的应用程序中写下有关内存使用情况的详细信息。 Here 你会发现一部电影捕捉到一个进程分配内存,直到它的父容器被 OOM 杀死。相应的 .NET 应用程序不时将其使用的内存量写入控制台,即使在容器不再存在后 Kubernetes 日志也会显示,从而可以查看发生了什么
  • 限制应用程序以使其处理少量数据(例如,如果您每分钟仅处理 1 张图片,请暂时从内存的角度来看会发生什么情况)
  • 查看详细的OOM killer内核日志,可以看到cgroup中的所有进程。在一个容器内拥有多个进程是完全有效的(就像在其他进程中一样,除了该容器中 PID 为 1 的进程)并且 OOM 杀手可以很好地杀死其中任何一个。在这种情况下,您可能会偶然发现 unexpected twists。然而在您的场景中,似乎是主进程终止了,否则容器不会被 OOMkilled,所以这种场景不太可能。

只是为了完整起见:底层框架可以强制执行低于容器内存限制的限制。例如。在 .NET 中,当 运行 在具有内存限制的容器中时,这是 75%。换句话说,在限制为 2,000 MiB 的容器内分配内存的 .NET 应用程序将在 1,500 MiB 时出错。然而在那种情况下,您会得到一个退出代码 139 (SIGSEGV)。这似乎不适用于此处,因为 OOM 终止程序终止了进程,并且从内核日志中可以清楚地看到所有 1 GiB 都已实际使用 (anon-rss:1043688kB)。据我所知,Go 还没有类似的设置,尽管社区一再要求它。