容器应用崩了,日志里只剩一行exit code 139,剩下的全是空白。你想进到容器里看看进程状态、跑几个命令定位问题,却发现 Azure Container App 不像传统 VM 那样给你开 SSH。这是不少人第一次被 Azure Container App 的 Debug Console “救回来”的场景。
Debug Console 是 Azure Container App 内置的交互式调试入口,它的作用很直接:让你在运行中的容器实例里执行命令,查看环境变量、进程列表、网络连接、文件系统等。它不会替你做排障,但能给足“视野”,把问题从“看不见摸不着”变成“看得见摸得着”。这篇文章适合折腾过 Azure Container App、但总觉得排障手段不够的开发者,也适合刚接触容器应用、想搞明白出问题时有哪些后路的运维朋友。我会从打开方式讲起,把容器里能用的调试工具、常见排障链路、权限与限制一起说透。
1. 无 SSH 时代的排障困境:Debug Console 到底解决了什么
1.1 传统服务器和容器应用之间最难受的差异
以前管虚拟机,出问题第一反应就是 SSH 上去,ps aux、df -h、tail -f挨个跑一遍。但 Azure Container App 是 Serverless 容器平台,默认不提供 SSH 入口,甚至不保证你有一个“长期存在”的实例。容器实例随时可能因为扩容、缩容、更新、重启而重建。你还没来得及登录,它可能已经没了。
这就带来一个很现实的尴尬:当应用起不来、反复崩溃、网络不通时,你手上通常只有三样东西——日志、指标、配置。日志如果写得不全,指标只能说明“有异常”,配置只是“你以为你配置对了”。真正运行时的状态,比如环境变量到底注入成什么样、配置文件路径对不对、DNS 解析到了哪个 IP、进程是不是内存吃爆了,日志里往往完全没有答案。
Debug Console 解决的就是这个信息断层。它给你一个进入正在运行的容器实例的通道,让你像在普通 Linux 机器上一样,直接执行命令查看现场。不需要修改代码,不需要重新发布新版本,也不会影响其他实例。
1.2 Debug Console 的能力边界
我习惯把 Debug Console 理解成“一个临时的、受限的观察窗口”。它和传统 SSH 有本质区别:
- 它不是一个独立服务,而是容器实例内的一个交互式 Shell 进程
- 它依托于容器镜像里自带的
/bin/sh或/bin/bash,镜像里没有 Shell 就没法用 - 你能在容器里看到的文件系统、进程、网络栈,都是这个实例的真实状态
- 但你在里面做的任何修改都不会“固化”成新的镜像或配置,容器重启后一切回到原样
它的核心价值在于“看”和“验”。看实际运行状态,验证你的配置假设。至于修改,那是最后一步才需要考虑的事。这一点想清楚,你就能理解为什么说 Debug Console 是排障利器,但不是运维逃生舱。
1.3 哪些场景必须靠 Debug Console 收场
我实际用下来的经验,下面这几类问题靠日志几乎不可能定位,Debug Console 基本是必需的:
- 容器反复重启(CrashLoopBackOff):日志只显示进程退出了,但看不到原因。需要抢在实例被回收前进去手工跑一次启动命令,把真实报错抓出来。
- 配置与预期不符:环境变量、挂载的配置文件、启动命令写进 Bicep/Terraform 后到底变成了什么,直接进容器核对是最快的。
- 网络连通性问题:应用能启动但调用依赖服务超时。光看日志只能看到一堆 Timeout,但你完全不知道是 DNS 解析失败、TCP 被拒、还是握手阶段出问题。
- 资源与性能问题:实例内存或 CPU 使用率冲到 100%,但不知道是哪个进程、哪个线程在消耗。
这四类我后面都会用真实排查链路展开讲。先看一下怎么打开这个 Console。
2. 打开 Debug Console 的两种姿势:Portal 点点点与 CLI 一条命令
2.1 Azure Portal 里的操作步骤
Portal 方式的路径不算复杂,操作步骤如下:
在 Azure Portal 中进入目标 Container App 的页面,找到左侧菜单中的“Console”入口(新版本 Portal 也会标成“Debug Console”)。点击后,界面会让你选择一个具体的容器实例。如果有多个副本(Replica)或 Revision,需要先选定一个目标实例,再选择要进入的容器。
点“Connect”之后,浏览器会打开一个 Web 终端。这个过程通常需要几秒钟,期间 Portal 会建立到容器实例的连接,在容器里启动/bin/sh。看到#提示符就说明成功了。
有个实际操作中的细节容易忽略:如果你的应用有多个副本,你进的是其中一个实例,而不是所有实例。排查时如果问题在特定实例上出现,你得选中那个状态的实例,而不是随便挑一个。另外,Portal 里显示的实例列表不一定稳定,容器崩溃重启后实例会被移除重建,这直接影响你能否抢时间进去。
2.2 命令行方式:az containerapp exec
如果整天泡在终端里,我更推荐用 Azure CLI。一条命令:
az containerapp exec \ --name my-container-app \ --resource-group my-resource-group执行后 CLI 会找到第一个容器实例并直接进入交互式 Shell。如果你想指定容器或实例,CLI 也支持参数,比如:
az containerapp exec \ --name my-container-app \ --resource-group my-resource-group \ --container my-container和 Portal 相比,CLI 的好处是你可以从本地终端直接进入,复制粘贴命令更方便,尤其适合做复杂排查时。它还适合自动化运维场景,可以在脚本里拼上几条排查命令,一次性把现场快照拉下来。
顺带提一个高级替代方式:如果你的 Container App 启用了虚拟网络集成,az containerapp ssh命令也可以建立基于 SSH 协议的通道。实际用起来,与 exec 的体验差异不大,但 SSH 方式对网络和生产环境下受控环境支持得更稳一些。日常排查用exec足够。
2.3 Debug Console 的底层机制:不是黑魔法
很多人好奇这个 Console 到底是怎么实现远程连接到容器里的。它的本质并不复杂:Azure Container App 底层运行在 Kubernetes 集群上,Console 就是通过 Kubernetes API 的 exec 机制,在工作负载对应的 Pod 里启动一个/bin/sh进程,然后把进程的标准输入、标准输出、标准错误流通过 WebSocket 转发到你的浏览器或 CLI。
所以它有两个天然的约束条件:
- 容器镜像里必须存在可执行的 Shell,比如
/bin/sh或/bin/bash。如果你的镜像用了distroless(Google 那类极简镜像)或仅包含静态编译二进制文件,它就没有 Shell 可启动,这时 Console 会直接报错或黑屏。 - 当前运行账号必须允许启动进程。容器以非 root 用户运行时,至少需要该用户在容器内有执行 Shell 的权限。
理解了这个机制,你就知道遇到“Console连不上”时该往哪个方向排查:先看镜像类型,再看运行时用户配置,而不是一开始就怀疑是不是 Portal 出了问题。
3. 容器里能用的调试工具:你的镜像决定了你的武器库
3.1 先摸清家底:这个容器里到底有什么
Debug Console 里的工具集完全继承自容器镜像。不同基础镜像差异非常大。
| 基础镜像 | Shell | curl | wget | ping | nslookup/dig | nc |
|---|---|---|---|---|---|---|
| Alpine | /bin/sh(busybox) | 无 | 有(busybox) | 有(busybox) | 无 | 无 |
| Ubuntu/Debian | /bin/bash | 有 | 有 | 有 | 视版本可能有 | 默认无 |
| Distroless | 无 | 无 | 无 | 无 | 无 | 无 |
| CentOS/RHEL | /bin/bash | 默认有 | 有 | 有 | 可能无 | 默认无 |
我习惯进入 Console 后首先跑一条“摸底”命令,把这几个东西都过一遍:
ls -l /bin/sh /bin/bash /usr/bin/curl /usr/bin/wget /usr/bin/nc 2>/dev/null这样能在一秒内判断出你手里有哪些牌。如果遇到完全没工具的情况,《基础环境》里也有一条路可走:用 Shell 内建能力或/dev/tcp完成很多原本依赖外部工具才能做的事,后面专门讲。
3.2 基础信息收集工具:env、cat、ps、df、mount
这些工具几乎是 Linux 排障的标配。Debug Console 里最常用的组合我列一下:
env:列出全部环境变量。应用启动前注入的环境变量,在这里能看到最终结果。注意关注像PORT、DATABASE_URL、CONNECTION_STRING这类关键值。cat/ls -l:看配置文件是否真的挂载到了预期路径,权限是否正确。我曾经排查过一个配置不生效的问题,最后发现是配置文件挂载路径里多了一个度娘都搜不出来的中文字符。ps aux:列出所有进程。重点看 PID 1 是谁、进程以什么用户跑、有没有 zombie 进程。df -h:查看文件系统剩余空间。很多应用在磁盘写满后表现出的症状极其诡异,比如请求挂起、偶发 500,而不是直接报“磁盘满”。mount:确认挂载点和数据卷的情况。id:确认当前用户的 UID/GID,排查权限问题时非常有用。
这里有一个容易踩的坑:top在精简镜像里不一定存在。如果top不识别,可以直接到/proc里抓信息:
cat /proc/loadavg cat /proc/meminfo3.3 网络诊断工具:curl、nslookup、nc 的实战用法
网络问题最常用的是 curl。测试一个 HTTP 端点时,我一般不会只用一个curl -v,而是分层做:
curl -v -o /dev/null -s http://example-api.internal-v会把 DNS 解析、TCP 连接、TLS 握手、HTTP 响应头全部打出来。看输出你能区分问题到底卡在哪一层。如果 DNS 解析失败,输出里会有Could not resolve host;如果 TCP 连不上,会有Connection refused或Connection timed out;如果 TLS 有问题,会卡在握手阶段直接报证书错误。
DNS 解析单独测试用nslookup或dig。但很多剪裁过的镜像里面根本没有这两个命令,这时可以用getent hosts,来自 GNU libc,在很多 Linux 基础镜像里都存在:
getent hosts db.internal如果连getent都没有,还可以直接让 curl 来处理,从-v输出的Trying 1.2.3.4:5432...一眼就能看出解析对没对。
TCP 端口连通性可以用nc -zv <host> <port>来测。同样地,精简镜像可能没有nc,此时完全可以用 bash 内建的/dev/tcp替代:
timeout 5 bash -c 'echo > /dev/tcp/10.0.0.5/5432' && echo "open" || echo "closed"这个技巧在只有 busybox 或甚至只有 bash 的环境里特别好用,是纯内建能力,不依赖任何外部二进制。我已经不止一次在 Debug Console 里用它救场。
3.4 运行时环境检查:Node、Python、Java、.NET 各有各的招式
如果你排查的是现代应用容器,除了系统层面的工具,还需要看运行时层面的信息。
- Node.js 应用:
node -v看版本,ps aux | grep node看启动参数,如果需要排查模块问题,npm ls --depth=0可以看顶层依赖树。内存问题优先看NODE_OPTIONS里有没有设--max-old-space-size。 - Python 应用:
python --version、pip freeze看已安装的包。启动文件是否存在、工作目录是否正确,可以在 Console 里直接ls确认。 - Java 应用:如果镜像里带了 JDK,
jstat -gcutil <pid>可以看 GC 情况,jstack <pid>可以看线程堆栈。如果只有 JRE,很多诊断命令不可用,这时只能依赖应用日志。 - .NET 应用:
dotnet --info查看安装信息。若应用托管在 ASP.NET Core 环境,dotnet-counters这类工具未必内置在镜像里,Console 里跑不了的话不要死磕。
3.5 工具缺失时怎么办:两条实在的退路
第一种退路:临时装包。如果你的基础镜像是 Alpine,理论上可以:
apk add --no-cache curlUbuntu 系列是:
apt-get update && apt-get install -y curl但这里有个大前提:你的网络必须具备访问外部软件源的出口。很多企业环境对出网有严格限制,这个操作经常超时或者被防火墙拦掉,所以别把希望都寄托在临时装工具上。
第二种退路:善用 Shell 内建能力和基础文件系统。比如端口测试用/dev/tcp,HTTP 请求用curl不行时,如果你的镜像里有 bash 且应用依赖 HTTP 接口,可以用exec 3<>/dev/tcp/host/port手工发一个 HTTP 请求:
exec 3<>/dev/tcp/10.0.0.5/8080 printf 'GET /health HTTP/1.0\r\nHost: app\r\n\r\n' >&3 cat <&3这个玩法对付最简单的 HTTP 健康检查场景足够用了。总之,Debug Console 里工具不够的时候,思路应该是“用系统自带能力代替外部工具”,而不是硬等镜像重做。
4. 四个生产环境排障实例:从崩溃循环到网络黑洞
4.1 实例反复重启:抢在消失前抓到真实报错
有次我负责的一个网关应用在更新后进入 CrashLoopBackOff。日志里只有一行terminated with exit code 139,完全没有堆栈。我的第一反应是看新版本的启动配置里有没有引入除零的违规操作,但很快发现日志根本不输出进程内部的初始化异常。
我沿着下面这条链路排查:
- 打开 Debug Console,进入一个正在重启的实例。这一步要快,因为实例可能随时消失。
- 先看 PID 1 是什么:
ps aux,发现 PID 1 是一个包装脚本,而不是应用进程本身。 - 手工执行这个包装脚本:
终端直接打印出真实错误:原来是脚本尝试读取一个不存在的秘钥文件,退出码 139 只是核心内存段的通用表现。/entrypoint.sh - 再看文件系统,
ls -l /secrets/,发现挂载路径和代码里读取的路径不一致,差了一级目录。
这个问题的根源是配置和代码路径不同步,日志把真正的错误吞掉了。如果没有 Console 手工跑一次启动命令,光看日志永远定位不到这一层。
4.2 环境变量与配置文件“没生效”:进容器对账
另一个案例是应用能启动,但行为明显不符合预期。配置中心明明设置了新的数据库连接地址,应用却还是一直连旧库。团队里的开发同学反复确认代码没改、配置已发布,一副“不可能”的样子。
我用 Debug Console 直接进容器“对账”:
env | grep -i database结果环境变量里根本没有新地址,而是出现了一个拼写错误:
DATABASE_CONNECTION_STRIN_G=sbq://wrong-server:5432问题出在应用配置模板里变量名的尾巴多打了一个下划线。代码使用的DATABASE_CONNECTION_STRING永远是空值,于是应用直接走了默认值——连接旧库。这种问题靠日志排查简直是大海捞针,但一条env命令 30 秒解决。
要注意的是,在 Console 里通过export修改的变量只影响当前 Shell,不会改到容器配置,更不能指望它持久化。发现问题后还是得回到配置定义处修复并重新部署。
4.3 网络连接异常:分步拆解 DNS、TCP、TLS
生产环境下网络故障是最难排查的问题之一。有一次,服务 A 调用服务 B 频繁超时,B 返回的日志里没有任何异常,A 的日志里全是Connection timed out。我用 Debug Console 在 A 的实例里做了一次分步测试。
第一步测 DNS:
getent hosts svc-b.internal输出正常,解析到了内网 IP。说明 DNS 没问题,排除域名误解析。
第二步测 TCP 端口:
timeout 5 bash -c 'echo > /dev/tcp/svc-b.internal/443' && echo "open" || echo "closed"结果竟然是closed。这下问题收窄了:不是应用问题,而是 443 端口根本连不上。
第三步看路由和防火墙。Azure 环境里,容器之间走的就是虚拟网络。最终发现是网络安全组(NSG)规则只放行了特定来源网段到服务 B 的 443 端口,而服务 A 所在的子网网段不在白名单里。修好 NSG 后,连接立即恢复。
这个链路最大的价值在于:用命令把“连接超时”一步步拆开,分别定位 DNS、TCP、TLS 各自的状态,这比盲目改超时配置靠谱得多。
4.4 内存耗尽:OOMKilled 背后的真正元凶
还有一个让我印象深刻的案例:某定时任务的容器持续被 OOMKilled。Java 应用,内存参数配置确实存在问题。但 Debug Console 的价值在于,它让我在没有修改代码的情况下确认了元凶是堆外内存暴涨。
现场操作:
free -m看到可用内存为 0,大部分被 buff/cache 占据。随后:
ps aux --sort=-%mem发现除了 Java 进程外,还有一个内部代理进程占用了将近 300MB,而这部分内存完全不在 JVM 堆参数控制范围内。最终确认是代理进程的内存泄漏导致堆外暴涨,与 JVM 配置无关。这个结论如果只靠外面看指标,会误掉进“堆栈调参”的大坑。
Console 里还能看更细的线程和 FD 信息:
cat /proc/<pid>/status | grep -E "VmRSS|Threads|FDSize"OOM 问题往往需要多维度信息交叉验证,单看内存总量不够,进程级画像才是关键。
5. 权限、超时与镜像依赖:使用 Debug Console 必须知道的边界
5.1 不是你拿到 Azure 账号就能点开
首先明确一点:Debug Console 不是一个全账号开放的功能,它受 Azure RBAC 控制。要能连接容器实例并执行命令,你的角色必须包含Microsoft.App/containerApps/exec/action这个操作。
如果你用的是 Contributor 角色,默认是有的。但如果团队用了最小权限策略,只有 Reader 或自定义角色,你会发现 Portal 里 Console 按钮可能直接灰掉,或者点击后报权限不足。
我遇到过团队同事被这个卡住,自定义角色里漏了这一项。排查办法是去 Azure Portal 的 Access Control (IAM) 里确认你的角色定义,必要时请管理员为自定义角色加上Microsoft.App/containerApps/exec/action。这不是什么复杂操作,但漏掉的人不少。
5.2 会话超时与实例生命周期:Console 不是持久连接
Debug Console 建立的 Shell 会话绑定在具体实例上。只要发生下面任意一种情况,会话就会中断:
- 容器实例被重启、重建(比如更新 Revision、触发扩缩容)
- 容器应用因故障被自动替换实例
- Portal Web 终端空闲时间过长,被会话超时机制断开
- 你的本地网络中断,导致 WebSocket 连接断开
所以正确的排查姿势是:打开 Console 尽量高效操作,先把重要信息抓下来,比如进程列表、环境变量、网络状态,最好是复制到本地保存,而不是看着终端发呆。
5.3 镜像依赖的硬约束:distroless 与 Windows 容器
前面提过,Debug Console 依赖容器内的 Shell。如果你的应用镜像基于distroless打完的,那这个功能对你基本就是不可用的。Go 应用常用这种方式把镜像体积压得非常小,生成出来的镜像里既没有 bash 也没有 sh,甚至连执行权限管理都很严格。
遇到这种镜像要怎么办?我的经验是两条路:
- 在非生产环境临时用一个带调试工具的镜像替换一次,比如切换到
ubuntu:22.04为基础镜像重新构建,定位完问题后再换回去。这需要重新构建发布,成本稍高。 - 依赖应用自身的健康检查和日志输出,把问题定位重心移到日志结构化上。
另外,Azure Container App 对 Windows 容器的支持目前与 Linux 场景有差异,Debug Console 对 Windows 容器的支持我不建议作为主要依赖。如果你的容器是 Windows,先按照应用日志和平台指标排查是更稳妥的路线。
5.4 我的几条使用心得
用 Debug Console 这么久,最后分享几条实在操作建议:
- 进去后先
date看容器时间,这个细节经常被忽略。容器时区如果和日志系统不一致,分析时间线的时候会被带偏。 - 不要一上来就
vim或者乱改文件。Console 里的修改不会进入镜像,改了只可能让当前实例更异常,排查完后还是要回到 IaC 文件修复。 - 抓现场信息时尽量“成批拉取”,比如:
env > /tmp/env.txt ps aux > /tmp/ps.txt cat /etc/resolv.conf > /tmp/dns.txt然后把这些文件内容复制出来留档。因为容器随时可能重启,先保存到本地再慢慢分析,是最安全的做法。
- 遇到 Debug Console 本身连不上,别急着提工单。先自查镜像里有没有 Shell、账号角色里有没有 exec 权限、实例是否处于 Running 状态。这三条检查完,九成问题都出在这。
Debug Console 听起来是个小功能,但在生产环境真正出事的时候,它就是那个让你不至于两眼一抹黑的窗口。能熟练用好它,排障效率会明显不一样。