ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

containerd 安装配置与运维实战:K8s 节点从 Docker 迁移踩坑指南

containerd 安装配置与运维实战:K8s 节点从 Docker 迁移踩坑指南 containerd 这个角色我最早是从 Docker 的进程树里认出来的。当时执行ps -ef | grep containerd发现真正在底下承接容器生命周期的是 containerd 和 containerd-shimDocker 的守护进程更像是站在前面收指令的那一层。后来在裸机上搭 Kubernetes把节点上的 Docker 摘掉、让 kubelet 直接对接 containerd这件事我前后做过十几遍apt 源装过、官方二进制包装过、内网完全离线的手工部署也搓过中间踩的坑足够写成一篇长文。这篇就把安装路径、配置项的取舍、以及我遇到过的每一条报错摊开讲清楚。不管你刚接触容器、还没搞明白 containerd 和 Docker 是什么关系还是已经在生产节点上运维、被depends: containerd ( 1.2.6-0ubuntu1~)这类依赖报错卡了半小时应该都能从这里面对上号。1. 先把 containerd 这个角色讲明白1.1 它在容器技术栈里到底站哪个位置很多人第一次接触这个概念会有点懵明明装了 Docker怎么又冒出来一个 containerd。用一句话概括Docker 负责的是用户体验containerd 负责的是干活。当你敲下docker run的那一刻Docker 守护进程解析参数、准备好镜像和挂载信息然后把真正创建容器、拉起进程、管理生命周期这件事交给 containerdcontainerd 再通过 shim 进程去调 runcrunc 按照 OCI 标准把容器进程 fork 出来自己退出。整个链条是 Docker daemon → containerd → containerd-shim → runc → 容器进程。这层结构带来的最大好处是解耦。Kubernetes 从 1.24 版本开始正式移除 dockershimkubelet 直接通过 CRI 接口找 containerd 说话中间少了一层翻译链路更短、故障点更少、资源占用也更低。我实测过同一台 4 核 8G 的机器只跑 containerd 比跑完整 Docker containerd 组合常驻内存能省下几百兆容器启动延迟在密集调度场景下也能感觉到差别。理解了这层关系后面很多问题就顺了。比如为什么ctr命令能看到镜像、docker images却看不到——因为它们本来就是两套客户端在跟同一个后端的不同命名空间对话。1.2 单独装 containerd 到底图什么如果你的场景只是本地开发、跑几个容器玩一玩老实说 Docker 更省心装完就能用命令也熟悉。真正值得单独部署 containerd 的情况大概有三类。第一类是 Kubernetes 节点。既然 dockershim 已经移除节点上装 Docker 纯属多此一举还会带来 cgroup 驱动不一致、镜像存储冗余的问题。第二类是资源敏感的服务器比如一台机器上要塞几十上百个容器少跑一个守护进程就是实打实的资源。第三类是对版本可控性要求高的生产环境包管理器里的版本通常滞后你想用 1.7 的某些新特性就得自己掌管安装包。我自己的判断标准很简单这台机器上有没有编排系统在调度容器。有就用 containerd没有Docker 更顺手。1.3 版本怎么选留一条能回退的路containerd 的版本节奏跟 Kubernetes 是绑定的选版本不要看最新要看目标集群支持到哪。1.6 系列稳定但偏老1.7 系列是目前大多数发行版和 K8s 1.28 到 1.31 的推荐搭配。我的习惯是先确认 kubelet 版本再往上查官方兼容矩阵然后在这个范围内选一个次版本号最新的补丁版。还有一个容易被忽视的点——config.toml的版本字段。containerd 1.7 默认生成的是version 2的配置而网上大量教程还停留在version 1的语法直接照抄会把服务搞挂。这个问题我在第 6 节会展开讲。留后路的做法很朴素装之前把旧版本的二进制和配置文件都备份到/opt/bak/下面配好包管理器的版本锁定。真出事的时候回退比重装快十倍。2. 动手之前这四件事先确认清楚2.1 系统信息与内核依赖核查第一步不是敲安装命令是把环境摸清楚。我固定会跑这几条uname -r # 内核版本5.x 以下很多特性受限 cat /etc/os-release # 发行版和版本号 systemctl status containerd # 是否已经装过 which runc containerd ctr # 有没有残留二进制 ls /etc/containerd/ # 有没有遗留配置 df -h /var/lib/containerd # 数据目录空间内核这块overlayfs 是必须的绝大多数 4.x 以上内核都自带。要额外确认的是br_netfilter和overlay两个模块有没有加载K8s 场景下还需要net.bridge.bridge-nf-call-iptables这类转发参数。这些不是 containerd 自己的要求而是容器网络的基础提前配好能省掉后面一堆容器起来了但网络不通的排查。另外要注意的是很多云厂商的镜像里预装了 Docker而 Docker 又自带一个 containerd。这种状态下直接装新版本会出现两套二进制、两套 systemd 单元的混乱局面必须先把环境清干净再动手。2.2 源里的 containerd 和 docker.io 到底是什么关系这是踩坑最多的地方热词里那条docker.io : depends: containerd ( 1.2.6-0ubuntu1~)就是典型症状。Ubuntu 官方源里的docker.io是一个打包好的 Docker 发行版它声明了对containerd这个包的依赖版本要求是 1.2.6-0ubuntu1~。当你后面又从 Docker 官方源装了containerd.io两个包都提供 containerd 二进制但包名不同apt 的依赖解析就会打架。轻则安装失败报依赖不满足重则两个包互相覆盖文件dpkg数据库出现不一致。我的处理原则是全机器统一来源绝不混装。要么全走发行版源docker.iocontainerd要么全走 Docker 官方源docker-cecontainerd.io。混用的代价远大于省下的那点配置时间。诊断命令很实用apt-cache policy containerd containerd.io docker.io dpkg -l | grep -E containerd|docker apt-get checkapt-cache policy会把每个包的候选版本、已装版本、来自哪个源全部列出来一眼就能看出是不是版本墙上冲突了。2.3 目录规划与磁盘占用预估containerd 的数据默认落在/var/lib/containerd这里面装的是镜像层、快照、元数据。它的空间占用有个特点镜像层是共享的同一个基础镜像被多个镜像引用时只存一份所以实际占用往往比docker images显示的累计大小要小。但如果你的机器要跑几十个不同镜像那就得认真估算了。我的经验值是常规业务镜像 200MB 到 1GB 不等加上构建缓存和临时层一台业务节点预留 100G 以上比较从容。还要注意快照器的选择。默认是overlayfs性能好、层数少。老内核上可能退化到native或devmapper后者需要额外配置 thin pool属于另一种复杂度。装完后我一般会跑一条containerd config dump | grep snapshotter确认真实生效的是哪个。2.4 时间同步和证书这件事别忽略这个坑比较隐蔽。容器镜像仓库、私有 Harbor、TLS 证书校验全都依赖系统时间。机器时间漂了几分钟拉镜像时就会报x509: certificate has expired or is not yet valid报错看起来像证书坏了其实是时钟歪了。装之前先确认timedatectl status systemctl status systemd-timesyncd离线内网环境尤其要注意通常内网有自己的时间源。装完 containerd 再回头查这个问题会浪费大量时间在不该怀疑的方向上。3. 三条安装路线按环境挑一条3.1 路线一包管理器安装最快也最容易被源坑这是新手上手最快的方式。Debian / Ubuntu 系直接用发行版源apt-get update apt-get install -y containerd systemctl enable --now containerdCentOS / Rocky / Alma 系dnf install -y containerd.io如果想用更新的版本就换成 Docker 官方源Ubuntu 下的完整步骤是apt-get update apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod ar /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable \ /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y containerd.io这条路线的优点是包管理器负责依赖、升级和卸载异常省心。缺点也很明确版本受源限制不同源之间容易冲突docker.io和containerd.io打架就是从这里来的。注意如果机器上已经装了docker.io再装containerd.io之前先执行apt-get remove --purge docker.io或者反过来保持只用一套。看到unmet dependencies别急着--force先看清楚是谁在依赖谁。3.2 路线二官方二进制包安装可控性最高生产节点我基本都走这条路。从 GitHub Releases 下载官方压缩包解压到/usr/local配置文件、systemd 单元全都自己掌舵版本在文件路径里看得清清楚楚。VERSION1.7.20 wget https://github.com/containerd/containerd/releases/download/v${VERSION}/containerd-${VERSION}-linux-amd64.tar.gz tar Cxzvf /usr/local containerd-${VERSION}-linux-amd64.tar.gz注意这个包里不包含 runc需要单独装RUNC_VERSION1.1.14 wget https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.amd64 install -m 755 runc.amd64 /usr/local/sbin/runcrunc 版本和 containerd 版本之间也有兼容性我一般按官方文档给出的建议搭配不盲目追新。然后装 systemd 单元文件。官方仓库里有现成的containerd.service下载到/usr/local/lib/systemd/system/下面wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service \ -O /usr/local/lib/systemd/system/containerd.service systemctl daemon-reload systemctl enable --now containerd systemctl status containerd --no-pager这条路线的好处是任何一步出问题都能定位到具体文件升级时直接替换二进制、systemctl restart就行。代价是要自己盯着 runc 的更新因为 runc 出过的容器逃逸类问题不算少运维上必须跟上补丁。3.3 路线三离线内网环境手工部署内网机器上不了外网这套流程就要提前在有网的机器上把物料备齐containerd 压缩包、runc 二进制、systemd 单元文件、所需的 CNI 插件包、目标镜像的 OCI 归档。打包过去之后tar Cxzvf /usr/local containerd-1.7.20-linux-amd64.tar.gz install -m 755 runc.amd64 /usr/local/sbin/runc cp containerd.service /usr/local/lib/systemd/system/ systemctl daemon-reload systemctl enable --now containerd镜像的搬运有两种做法。一种是ctr images export导出成 tar拷到目标机器ctr images import导入另一种是在内网搭一个私有仓库让节点直接拉。前者适合少量固定镜像后者适合长期维护。我做过一个 30 节点规模的内网环境最后选择了私有仓库方案节点侧只需要配好hosts.toml指向内网地址后续镜像更新不用再挨个机器拷文件。3.4 三条路线的对比维度包管理器安装官方二进制包离线手工部署上手速度最快一条命令中等5 到 10 分钟慢取决于物料准备版本可控性受源限制完全可控完全可控升级便利性一条命令搞定替换二进制重启需重新打包物料依赖冲突风险高容易混源低低适合场景测试机、快速验证生产节点、K8s内网、隔离环境常见坑点依赖打架、版本滞后忘记装 runc、单元文件缺失镜像搬运、证书信任这张表是我在几个不同环境里实践下来的总结。选路线的时候先问自己一句这台机器未来一年会不会频繁升级。会就走二进制不会包管理器足够。4. 配置文件的每一项都要能说出理由4.1 先生成默认配置再按需修改不要从零手写config.toml那是给自己找麻烦。正确做法是让 containerd 自己生成一份完整默认配置再对着改mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml生成之后先看一眼开头第一行的version字段。1.7 生成的是version 21.6 及以前是version 1。这两个版本的配置结构差异不小尤其是 registry 相关的段落在网上抄配置时必须先核对这一行否则服务会直接起不来。改配置之前先备份这个习惯救过我很多次cp /etc/containerd/config.toml /etc/containerd/config.toml.bak.$(date %F)4.2 版本字段和 CRI 插件的关系version 2不只是个标记它决定了 containerd 怎么解析整个 TOML 树。在 v2 里所有插件配置都挂在plugins下面比如plugins.io.containerd.grpc.v1.cri。而在 v1 里CRI 配置的路径写法不一样。如果你在 v2 的配置里塞了一段 v1 时代的 registry 写法containerd 启动时会报failed to load plugin io.containerd.grpc.v1.cri之类的错误日志里还会带上unknown field的提示。这类报错的排查顺序永远是先看version再看段落的层级结构最后才怀疑语法。CRI 插件本身也要确认处于启用状态。默认配置里它是开着的但如果你的配置文件里出现了disabled_plugins [cri]那 kubelet 就连不上表现为节点一直NotReady。这个问题的隐蔽性在于 containerd 服务本身是健康的systemctl status一切正常只有从 kubelet 视角才能看出问题。4.3 SystemdCgroup 这一项决定了节点稳不稳这是我最想强调的一项。在config.toml里找到[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup false把它改成truesed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml为什么必须改因为 kubelet 默认使用 systemd 作为 cgroup 驱动而 containerd 默认用 cgroupfs。两边驱动不一致时会出现资源统计错乱、Pod 被反复重启、节点在高负载下失联等一堆看起来毫无关联的故障。我在一个测试集群上就遇到过kubectl top node显示的内存用量和实际差了一倍多排查了很久才定位到是 cgroup 驱动没对齐。同一段配置里还有sandbox_image这个是 Pod 沙箱容器用的 pause 镜像。老配置里写的是已被弃用的地址新环境要换成registry.k8s.io/pause:3.9这类可用地址sed -i s#sandbox_image .*#sandbox_image registry.k8s.io/pause:3.9# /etc/containerd/config.toml改完这两处systemctl restart containerd再往下的兼容性问题基本就堵住了。4.4 镜像加速和私有仓库怎么配才不踩空containerd 1.7 之后registry 配置推荐走config_path方式比在config.toml里堆一长串 mirrors 清爽得多[plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d然后在/etc/containerd/certs.d/docker.io/hosts.toml里写server https://registry-1.docker.io [host.https://your-mirror.example.com] capabilities [pull, resolve]目录名必须和仓库域名严格对应比如要加速registry.example.com目录就得叫/etc/containerd/certs.d/registry.example.com/。这个细节我一开始没注意配了半天不生效ctr拉镜像还是走原来的地址最后才发现是路径名对不上。私有仓库走 HTTPS 但用的是自签证书就在同一个目录下放ca.crt如果是 HTTP 仓库则要在hosts.toml里额外声明skip_verify true或把地址写进server并标注对应能力。生产环境我强烈建议上正经证书跳过校验的做法只用来临时验证。4.5 拉起服务并确认真的在工作配置完成后systemctl daemon-reload systemctl restart containerd systemctl enable containerd systemctl status containerd --no-pager journalctl -u containerd -n 50 --no-pager确认三件事socket 文件存在、日志里没有error级别的报错、ctr version能正常返回。ls -l /run/containerd/containerd.sock ctr versionsocket 路径这一点要注意/run和/var/run在大多数发行版里是软链接关系指向同一个位置但有些工具配置文件里写的是绝对路径两边对应不上就会报连接失败。我个人的写法是统一用/run/containerd/containerd.sock因为它是 systemd 单元里实际声明的路径。5. 日常运维命令ctr、nerdctl、crictl 怎么分工5.1 ctr 的命名空间是第一个坑ctr是 containerd 自带的官方 CLI功能全但用起来生硬。最大的门槛是命名空间概念。ctr images ls默认查的是default命名空间而 Kubernetes 用的是k8s.io所以很多人查完发现是空的以为镜像丢了。ctr namespace ls ctr -n k8s.io images ls ctr -n k8s.io containers ls ctr -n k8s.io tasks ls我习惯在~/.bashrc里加一个别名省事alias ctrkctr -n k8s.io另一个坑是ctr run。它和docker run的语义差别很大需要在后台运行、需要指定 task很多参数写法也不同。日常调试容器我基本不用它宁可上 nerdctl。还有一点ctr直接调用 containerd 的 API不经过 CRI 层。这意味着你用ctr run创建的容器kubelet 是不认识的反之亦然。两边看到的镜像共享同一份 content store但容器的管理是分开的这个边界要分清楚不然会困惑于为什么我创建的容器在 K8s 里看不到。5.2 nerdctl 补齐 Docker 的使用手感nerdctl 是一个兼容 Docker 命令风格的客户端nerdctl run、nerdctl ps、nerdctl images、nerdctl compose这些都能用迁移成本几乎为零。VERSION1.7.6 wget https://github.com/containerd/nerdctl/releases/download/v${VERSION}/nerdctl-${VERSION}-linux-amd64.tar.gz tar Cxzvf /usr/local/bin nerdctl-${VERSION}-linux-amd64.tar.gz nerdctl version它默认也是连/run/containerd/containerd.sock不用额外配置。有几个细节和 Docker 不同默认命名空间是default要操作 K8s 的镜像得加-n k8s.ioCompose 支持需要单独装nerdctl-full或者额外组件构建功能用的是 BuildKit首次用需要把 buildkitd 跑起来。我用它最多的场景是排查。nerdctl -n k8s.io images比ctr的输出友好太多nerdctl exec进容器也比crictl exec顺手。它是那种平时不一定用但一上手就回不去的工具。5.3 crictl 与 kubelet 对接的正确姿势在 K8s 节点上crictl是排查 Pod 问题的正主因为它走的是和 kubelet 完全相同的 CRI 接口看到的东西和 kubelet 看到的一致。先写配置文件cat /etc/crictl.yaml EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF然后常用命令crictl pods crictl ps -a crictl images crictl logs container-id crictl inspect container-id crictl rmi --prune我平时排查 Pod 起不来的顺序是crictl pods看沙箱在不在crictl ps -a看容器状态状态是Exited就直接crictl logs捞日志日志为空就crictl inspect看退出码和配置。这套流程比从kubectl describe开始往下挖快得多因为它是从节点侧看的跳过了 API Server 那一层。有个细节crictl也有命名空间的概念但它默认就绑在 k8s 的上下文里不需要像ctr那样手动指定这点比ctr省心。6. 踩坑实录我遇到过的报错和排查过程6.1 依赖冲突那条报错到底怎么解docker.io : depends: containerd ( 1.2.6-0ubuntu1~)这条报错我在测试机上撞过三次每次的成因都不完全一样。第一次是机器上装了 Docker 官方源的containerd.io后来又去装发行版源的docker.io两个包都要提供 containerd 实现apt 解析不动。第二次是发行版源里的containerd包版本太老低于docker.io声明的最低要求。第三次最坑是之前手动删过/usr/lib/systemd/system/containerd.service包状态是已安装但文件不完整dpkg校验失败。排查顺序我整理成这样dpkg -l | grep -E containerd|docker # 看包状态 apt-cache policy containerd containerd.io # 看候选版本和来源 apt-get check # 看依赖树是否自洽 dpkg --audit # 看有没有半装状态的包对应的解法来源混装就统一到一个源把另一套apt-get remove --purge干净版本不够就升级源里的containerd包状态损坏就apt-get install --reinstall containerd修复。绝对不要用--force-yes跳过依赖检查我见过这么干之后系统日志里全是二进制版本不匹配的报错排查成本翻好几倍。6.2 服务起不来日志会告诉你答案systemctl start containerd之后状态是failed先看日志journalctl -u containerd -n 100 --no-pager按报错内容分几类。出现toml: line X: expected ...就是配置文件语法错了通常是缩进或者引号问题TOML 对字符串里的反斜杠特别敏感Windows 路径风格的写法会直接炸。出现unknown field说明配置字段在当前版本不存在多半是把其他版本的写法抄进来了。出现failed to load plugin就去看那一段插件的层级对不对。还有一种情况是服务显示active但 socket 不存在。这通常是 socket 路径被改过或者containerd.service里的ExecStart参数和你的预期不一致。systemctl cat containerd可以把实际生效的单元文件完整打出来比猜快。端口和权限问题偶尔也会碰到。自定义 socket 路径落在没有写权限的目录下containerd 启动会失败但日志不够直白这时候检查一下目录权限和 SELinux 状态。6.3 拉镜像失败先分清是哪一类镜像拉不下来报错信息看着都差不多但成因差别大。第一类是网络和 DNS。报错常见dial tcp: i/o timeout、no such host。先ping仓库域名再curl -I试试连通性再看/etc/resolv.conf。内网环境要确认 DNS 能不能解析到私有仓库。第二类是证书。报错带x509字样先查系统时间再查 CA 证书有没有导入到正确位置。自签证书要放在对应仓库目录下的ca.crt里放错目录等于没放。第三类是仓库地址或命名空间写法问题。镜像名里带端口号、带多层路径的时候目录结构的对应关系容易搞错。规范写法是仓库域名:端口/项目名/镜像名:标签containerd 会按第一段去certs.d下面找同名目录。排查的时候我会直接上ctr绕过 CRI 层ctr -n k8s.io images pull registry.example.com/app/api:v1.2.3这样报错信息更底层、更具体。如果ctr能拉下来但 kubelet 拉不下来那就是 CRI 配置的问题方向立刻清晰。6.4 shim 和 runc 那些奇怪的报错有一类报错看起来和安装没关系但根子上是安装时的疏漏。最典型的是failed to start shim: exec: runc: executable file not found in $PATH多半是走二进制路线时忘了装 runc或者装到了/usr/local/bin而 systemd 单元的PATH里没有这个目录。确认方式which runc runc --version systemctl show containerd -p Environment还有一种是 shim 进程残留。节点上跑过很多容器之后ps -ef | grep containerd-shim能看到一堆僵尸 shim正常情况 containerd 会自动回收但如果某个容器状态异常shim 会一直挂着占资源。处理方式是先确认没有业务容器受影响再清理对应进程。生产环境上做这个操作必须格外谨慎我在测试环境验证过才敢在线上执行。6.5 问题速查表现象常见成因排查命令处理方式装包报依赖不满足混用不同来源的包apt-cache policy containerd containerd.io统一到一个源卸载冲突包服务启动失败配置文件语法或字段错误journalctl -u containerd -n 100对照version字段修正层级socket 不存在路径不一致或权限问题systemctl cat containerd核对单元文件里的 socket 路径ctr 看不到镜像命名空间不对ctr namespace ls加-n k8s.io拉镜像超时DNS 或网络不通curl -I registry检查 DNS、代理、加速配置证书校验失败系统时间漂移或 CA 未导入timedatectl、openssl x509 -noout -dates同步时间、补 CA 证书节点 NotReadyCRI 插件被禁用或 cgroup 驱动不一致systemctl status kubelet检查配置里的插件与SystemdCgroupshim 找不到 runcrunc 未安装或不在 PATHwhich runc安装 runc 并确认路径资源统计异常cgroup 驱动不一致containerd config dump打开SystemdCgroup true磁盘被写满镜像与日志累积df -h、ctr images ls清理无用镜像限制日志轮转这张表是我用最久的排查清单几乎覆盖了节点运维中九成以上的常见故障。7. 一些不太写进官方文档的经验安装这件事文档能告诉你怎么做但告诉不了你哪里会绊倒你。我把自己反复踩过、也反复教给同事的几条记在这里。配置改完先跑一遍 dry check。containerd 没有专门的--check参数但可以用containerd config dump来验证配置能否被正确解析它能输出就说明语法至少没错。这个动作能做在restart之前省掉一次服务中断。升级 containerd 之前先确认节点上没有正在跑的构建任务。BuildKit 这类组件对后端的版本比较敏感升级过程中如果刚好在构建镜像容易留下状态不一致的历史层清理起来很麻烦。日志轮转要在装的时候就配好。containerd 的日志默认进 journald节点上容器一多journal 的体积增长很快。我给生产节点的做法是限制 journal 的最大占用同时对/var/lib/containerd单独挂载或者做容量监控避免哪天突然把根分区写满。备份配置这件事别只备份config.toml。certs.d目录下的仓库证书和hosts.toml同样重要一旦机器重装重新配一遍认证比恢复一个文件慢太多。我的做法是把/etc/containerd整个目录纳入配置管理改之前先提交一次。最后还有一条关于心态的。containerd 的报错信息普遍比 Docker 简略新手容易慌。真遇到搞不定的把日志级别调成debug再看一遍信息量会大得多sed -i s/^level .*/level debug/ /etc/containerd/config.toml systemctl restart containerd调完记得改回来debug 级别的日志在繁忙节点上写入量相当可观。
返回列表