一台机器能跑几个 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 | # ECS account-level setting (awsvpcTrunking; must be enabled before creating instances) |
先开开关,再起机器
这里有一个很容易忽略的点,AWS 文档写得很直白:只有在开启 awsvpcTrunking 之后新启动的 EC2 实例,才会拿到 trunk ENI 和更高的上限。之前已经启动的实例,无论做什么操作都拿不到。
也就是说,开关打开之后,老机器要换掉才算真正生效。放到 Terraform 里,就是让 ASG 显式依赖这个账号设置,保证新建环境时一定是先开开关、再起机器:
1 | resource "aws_autoscaling_group" "ecs_pool" { |
机型变量上写清楚约束
因为不是每种机型都支持,所以每个环境的机型变量都在描述里写死了这条约束,换机型的人一眼就能看到:
1 | variable "instance_type" { |
一段多余的 user_data
dev 集群最早是手工建的,后来才 import 进 Terraform。当时 launch template 的 user_data 里留着这么一段注释:
1 | # 如果要同时开启 Trunking(可选,但既然账户已开启,这个是多余的) |
真正起作用的是账号级的 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 | aws --no-cli-pager ecs list-account-settings \ |
value 是 enabled 就行。注意这个设置是按账号、按区域的,每个环境都要查。
2. 机器上有没有 trunk
开关开了不代表机器上就有 trunk(还记得”只有新启动的实例才生效”吗)。要看容器实例本身:
1 | aws --no-cli-pager ecs list-attributes \ |
有 trunk 的实例会带上 ecs.awsvpc-trunk-id 属性,同时还有 ecs.capability.task-eni-trunking 这个 capability。再用 describe-instances 看一眼网卡,应该是 interface 加 trunk 两块。
3. 用量和上限
branch ENI 可以按类型过滤出来:
1 | aws --no-cli-pager ec2 describe-network-interfaces \ |
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 和内存都够”就下结论。