一台机器能跑几个 awsvpc 任务:ECS ENI trunking(awsvpcTrunking)复盘

缘起

我们的 ECS 集群都是 EC2 启动类型,任务一律用 awsvpc 网络模式。

刚开始搭 dev 环境的时候就撞过一次墙:机器的 CPU、内存都还有富余,新任务却起不来,原因是 ENI 不够用了。当时的解法是打开 ECS 的 awsvpcTrunking(俗称 VPC trunk / ENI trunking),让一台 EC2 能挂更多的网卡。代价是:这个方案不是每种机型都支持。

半年多过去,环境从 dev 一路扩到 qa、preprod、prod,机型换了好几轮。某天下午我突然想起这件事:现在各个环境的 ECS 集群,还有没有 ENI 不够的隐患?换过的机型,还都支持 trunking 吗?

巧的是,同一天下午 prod 正好有个任务卡在 PROVISIONING 起不来,ENI 也一度被列为头号嫌疑。这篇就把 trunking 的原理、我们怎么落地的、怎么巡检,以及那次”差点冤枉了 ENI”的排查,一起记下来。

先算一笔账:awsvpc 模式下 ENI 就是任务数上限

awsvpc 模式下,每个任务都会拿到一块属于自己的 ENI(弹性网卡)和一个子网 IP。好处是任务可以有独立的安全组,在 VPC 里就像一台独立的机器。

问题在于,一台 EC2 能挂的 ENI 数量是有硬上限的,而且主网卡也算一块。所以不开 trunking 的情况下:

1
单机 awsvpc 任务上限 = 机型最大 ENI 数 - 1(主网卡)

拿我们用过的机型算一下:

机型 最大 ENI 数 不开 trunking 的任务上限
m6a.xlarge(4c16g) 4 3
c6a.4xlarge(16c32g) 8 7

一台 4 核 16G 的机器只能跑 3 个任务,对 Java 微服务来说这是非常亏的:我们一个服务的 task 通常也就预留 256 到 1024 的 CPU,按 CPU 算一台 m6a.xlarge 能放十几个,按 ENI 算只能放 3 个。ENI 会比 CPU 和内存先耗尽。

这就是当初 dev 撞墙的原因。

ENI trunking 是怎么回事

打开 awsvpcTrunking 之后,ECS 会给新启动的容器实例额外挂一块特殊的网卡,叫 trunk ENI。之后每个 awsvpc 任务拿到的不再是一块独立挂在实例上的普通 ENI,而是挂在 trunk 下面的 branch ENI。

branch ENI 不占实例的 ENI 槽位,所以单机任务上限一下子就上去了。以 AWS 文档里的数字为准:

机型 不开 trunking 开 trunking
m6a.large 2 10
m6a.xlarge 3 20
c6a.4xlarge 7 60
r6a.2xlarge 3 40

开了 trunking 的实例上,describe-instances 看到的网卡是两块:一块 interface(主网卡),一块 trunk。任务的 branch ENI 不会出现在实例的网卡列表里。

我们是怎么落地的

账号级开关

awsvpcTrunking 是一个 ECS 账号设置,按区域生效。我们在 Terraform 的 ECS 模块里把它设成账号默认值:

1
2
3
4
5
# ECS account-level setting (awsvpcTrunking; must be enabled before creating instances)
resource "aws_ecs_account_setting_default" "awsvpc_trunking" {
name = "awsvpcTrunking"
value = "enabled"
}

先开开关,再起机器

这里有一个很容易忽略的点,AWS 文档写得很直白:只有在开启 awsvpcTrunking 之后新启动的 EC2 实例,才会拿到 trunk ENI 和更高的上限。之前已经启动的实例,无论做什么操作都拿不到。

也就是说,开关打开之后,老机器要换掉才算真正生效。放到 Terraform 里,就是让 ASG 显式依赖这个账号设置,保证新建环境时一定是先开开关、再起机器:

1
2
3
4
5
6
7
8
9
resource "aws_autoscaling_group" "ecs_pool" {
for_each = var.capacity_pools

name = coalesce(each.value.asg_name, "${var.cluster_name}-${each.key}-asg")

depends_on = [aws_ecs_account_setting_default.awsvpc_trunking]

# ...
}

机型变量上写清楚约束

因为不是每种机型都支持,所以每个环境的机型变量都在描述里写死了这条约束,换机型的人一眼就能看到:

1
2
3
4
5
variable "instance_type" {
description = "EC2 instance type for ECS instances (must support awsvpc-trunking)"
type = string
default = "m6a.large"
}

一段多余的 user_data

dev 集群最早是手工建的,后来才 import 进 Terraform。当时 launch template 的 user_data 里留着这么一段注释:

1
2
# 如果要同时开启 Trunking(可选,但既然账户已开启,这个是多余的)
# echo ECS_ENABLE_HIGH_DENSITY_ENI=true >> /etc/ecs/ecs.config

真正起作用的是账号级的 awsvpcTrunking,以及 ECS-optimized AMI 里足够新的 agent。这行配置在我们的场景下没有意义,后来做 SSH 加固改 user_data 的时候顺手删掉了,免得后来的人以为 trunking 是靠它开的。

哪些机型和场景不支持

“不是每种机型都支持”具体是什么意思,值得单独列一下。以下来自 AWS 文档:

  • 整个家族都不支持的:c5n、d3、d3en、g3、g3s、g4dn、i3、i3en、inf1、m5dn、m5n、m5zn、mac1、r5b、r5n、r5dn、z1d 等。
  • 家族支持、个别型号不支持的:比如 c5.metal、m5.metal、r5.8xlarge、c5a.8xlarge 等。
  • 支持但上限明显偏低的:*-flex 系列。比如 m7i-flex.large 开了 trunking 也只有 4,m7i-flex.xlarge 只有 10,而同尺寸的 m7i 分别是 10 和 20。只看”支持”两个字,很容易踩到这里。
  • Windows 容器实例不支持。 只支持 Linux 的 ECS-optimized AMI(或满足 agent 版本要求的 Amazon Linux)。
  • 共享子网(shared subnet)里的实例不支持,会注册不上集群。
  • 实例要关闭基于资源名的 IPv4 DNS(A 记录)。
  • 任务必须和容器实例在同一个 VPC 里。

另外有一个安全上的细节:trunk ENI 使用的是 VPC 的默认安全组,由 ECS 管理。如果你们对默认安全组有统一的收紧策略,这里要心里有数。

我们目前用的 m6a、c6a、r6a 都在支持列表里,上限也是正常档位。

巡检:各环境现在有没有 ENI 隐患

回到开头那个问题。巡检分三步:开关有没有开、机器上有没有 trunk、用量离上限还有多远。

1. 账号开关

1
2
3
aws --no-cli-pager ecs list-account-settings \
--profile <env> --region ap-southeast-1 \
--name awsvpcTrunking --effective-settings

value 是 enabled 就行。注意这个设置是按账号、按区域的,每个环境都要查。

2. 机器上有没有 trunk

开关开了不代表机器上就有 trunk(还记得”只有新启动的实例才生效”吗)。要看容器实例本身:

1
2
3
4
5
aws --no-cli-pager ecs list-attributes \
--profile <env> --region ap-southeast-1 \
--cluster <cluster> \
--target-type container-instance \
--attribute-name ecs.awsvpc-trunk-id

有 trunk 的实例会带上 ecs.awsvpc-trunk-id 属性,同时还有 ecs.capability.task-eni-trunking 这个 capability。再用 describe-instances 看一眼网卡,应该是 interface 加 trunk 两块。

3. 用量和上限

branch ENI 可以按类型过滤出来:

1
2
3
4
aws --no-cli-pager ec2 describe-network-interfaces \
--profile <env> --region ap-southeast-1 \
--filters Name=interface-type,Values=branch \
--query 'length(NetworkInterfaces)'

branch ENI 挂在 trunk 下面,从这个接口里看不出它属于哪台实例。好在一个 awsvpc 任务就对应一块 branch ENI,所以按实例看用量,直接用容器实例的 runningTasksCount 就够了,再拿去对照文档里该机型的上限。

结果

环境 机型 / 节点 单机最多任务数 trunking 上限(每台) 真正更紧的资源
dev 1 台 m6a.xlarge 16 20 CPU(剩余 476 / 4096)
qa 1 台 m6a.xlarge 16 20 CPU(剩余 476 / 4096)
preprod 4 台 m6a.xlarge 9 20 部分节点 CPU 只剩 392
prod 2 台 c6a.4xlarge 15 60 很宽裕

所有环境开关都开着,所有容器实例都挂了 trunk,最近的服务事件里也没有任何网卡相关的放置失败。现在没有 ENI 问题。

但这张表更有意思的地方在于:dev、qa 单机跑着 16 个任务,如果没有 trunking,这台 m6a.xlarge 只能跑 3 个。也就是说,我们现在的机器规模,是建立在 trunking 之上的。哪天有人图便宜换了个不支持的机型,或者换成了 -flex 系列,dev 和 qa 会立刻有十几个任务起不来。

另外,dev 和 qa 离 20 的上限只差 4 个任务。现在先顶不住的是 CPU,但如果以后把 task 的 CPU 预留调小、往一台机器上塞更多服务,ENI 上限会重新变成天花板。

插曲:差点冤枉了 ENI

同一天下午,prod 的 gateway-service 做了一次部署,两个新任务卡在 PROVISIONING 二十多分钟,containerInstanceArn 是空的。

排查时 ENI 一度是头号嫌疑,理由听起来很充分:

  • 两台机器的 CPU 只用了 12%,内存 26%,资源明明够;
  • 任务是 awsvpc 模式,每个都要一块 branch ENI;
  • capacity provider 的 CapacityProviderReservation 冲到了 160(目标 80),ECS 觉得需要 3 台多机器。

CPU、内存都够,却还要加机器,那缺的”大概”就是 ENI 了。

这个推理有一个前提是对的:ECS 容器实例的资源记账里只有 CPU、MEMORY、PORTS、PORTS_UDP,没有 ENI。 所以 remainingResources 里看不出 ENI 是否耗尽,”资源够却放不下”确实可能是 ENI 的问题。

但嫌疑归嫌疑,要拿数字说话。逐项核对的结果是:

  • 子网 IP:两个私有子网都是 /18,空闲 IP 一万六千多,不是 IP 耗尽。
  • branch ENI:一共 28 块,正好对应 28 个运行中的任务;两台机器分别 15 和 13 个任务,离 c6a.4xlarge 的 60 很远。
  • CloudTrail:90 分钟内没有任何来自 ECS 的 CreateNetworkInterface 调用,也没有报错。ECS 根本没有开始给这两个任务准备网卡。
  • 任务详情:attachments 是空数组。如果是 ENI 创建失败,这里会有一条状态异常的 attachment。

这几条放在一起,结论就反过来了:任务根本没被放置到任何一台机器上,自然也谈不上准备网卡。ENI 被排除。

真正的原因和网络无关:那次部署把服务从 launchType: EC2 切到了 capacity provider,而两台老机器注册进集群时的 capacityProviderName 是 ec2,不归这个 capacity provider 管,capacity provider 的任务放不上去;ASG 的 max 又是 2,加不了新机器,就这样卡住了。这是另一个故事,这里不展开。

这次插曲让我记住的是:ENI 不够和”任务没被放置”在表面上长得很像,都是资源看起来够、任务却卡着。区分它们的办法,是看 ECS 有没有真的去创建网卡(CloudTrail 和任务的 attachments),而不是看指标去推理。

多说几句

第一,awsvpc 模式下,单机任务数先撞到的往往是 ENI,而不是 CPU 和内存。 不开 trunking,一台 4 核 16G 的机器只能跑 3 个任务。如果你的集群是 EC2 启动类型加 awsvpc,trunking 基本是必选项。

第二,开关只对新机器生效。 打开 awsvpcTrunking 之后,老实例无论如何都不会有 trunk,必须替换。Terraform 里让 ASG depends_on 这个账号设置,是一个便宜又有效的保险。

第三,”支持 trunking”要看两件事:在不在列表里,上限是多少。 整个家族不支持的(比如 c5n、g4dn)好避开;-flex 系列这种”支持但上限只有一半”的更容易踩。换机型之前,先去查那张表。

第四,巡检要查到实例层面。 账号开关开着,不等于每台机器都有 trunk。ecs.awsvpc-trunk-id 这个属性才是实例真的拿到了 trunk 的证据。

第五,ENI 不在 ECS 的资源记账里。 这意味着控制台和 remainingResources 都不会告诉你 ENI 快满了,需要自己按任务数对照上限去算。反过来,怀疑 ENI 时也要拿 branch ENI 数量、CloudTrail、任务 attachments 去证实,不要只凭”CPU 和内存都够”就下结论。

参考资料