资讯详情

资讯详情

K8s与Harbor脚本安装实践:从系统初始化到镜像仓库联调

最近在给内部一个模拟项目搭 k8s 环境时我连续在几台机器上重复敲同样的命令第二遍就意识到这套安装必须脚本化。标题里说的“k8s和harbor脚本安装的一系列操作”其实就是我这次做的一整套方案从系统初始化、Kubernetes 集群搭建到 Harbor 镜像仓库的独立部署和联调全部用脚本串起来。最终效果是原来折腾一整天的环境现在只需要十几分钟更重要的是出问题以后可以重跑、可以翻日志、可以离线部署。这篇文章就是我自己的完整笔记把拆分思路、关键参数、脚本片段和踩过的坑都写下来适合准备用脚本方式批量部署 Kubernetes 和私有仓库的运维或开发同学参考。1. 先谈整体设计为什么我把集群安装拆成五类脚本1.1 手工安装的痛点和脚本化的收益先说痛点。我第一次装这套环境时顺序是先初始化系统再装容器运行时然后初始化控制平面接着让工作节点加入最后部署仓库。每一个环节都有不少命令而且有些命令必须等前面某个服务 ready 才能继续中间夹杂着改配置、重启服务、确认状态。一个人盯着一台机器还凑合一旦换成 4-5 台节点或者需要测试环境重新搭建一遍手工操作就会漏掉细节点。比如少关一次 swap、selinux 策略没改、容器运行时和 kubelet 的 cgroup driver 没对齐后面 kubelet 就直接起不来而且日志信息很误导人。把这些操作脚本化好处是显而易见的第一可重复同一套脚本可以在任何一台干净的机器上还原出同样的环境第二可排错每个阶段打日志失败时能看到在哪一步挂掉而不是靠人脑回忆第三可离线把所有安装包和镜像提前准备好内网环境也能完整装起来。我这次把整套安装拆成了五类脚本并不是为了炫技而是让每段脚本只负责一个清晰边界这样任何一步出问题都能快速定位。1.2 安装阶段的拆解与脚本目录约定我给你看下最终脚本目录的布局后面每个章节基本就是沿着这个目录讲install-k8s-harbor/ ├── 00-init-sys.sh # 系统初始化swap、防火墙、内核参数 ├── 01-install-containerd.sh # 容器运行时 containerd 安装与配置 ├── 02-control-plane.sh # 控制平面 kubeadm init CNI 部署 ├── 03-join-node.sh # 工作节点加入 ├── 04-harbor/ │ ├── harbor-offline-installer-v2.10.0.tgz # Harbor 离线安装包 │ ├── harbor.yml # Harbor 配置模板 │ └── install-harbor.sh # Harbor 独立脚本 ├── 05-fullcheck.sh # 安装完成后的自检脚本 └── logs/ # 所有执行日志统一落盘我刻意把系统初始化、运行时安装、控制面初始化、节点加入、Harbor 部署拆开原因有两个。一是职责单一系统初始化脚本在每台机器上都要跑而控制面初始化只有第一个 master 节点跑如果写成一个巨型脚本跳流程会非常别扭。二是参数传递简单main master 节点初始化完成后会把 token、hash、certificate-key 写到一个共享文件join-command.txt里工作节点脚本读取这个文件后自动执行不需要人在终端之间复制粘贴。这个阶段的实践结论是不要试图写一个“一键装好所有东西”的保姆脚本除非你永远只部署一台机器。凡是涉及多节点、多角色的安装宁可多拆几个脚本把“什么时候在哪台机器上跑哪个脚本”作为部署文档的一部分执行起来反而更可靠。2. 系统初始化脚本swap、防火墙和 cgroup driver 不能各装各的2.1 版本组合和资源规划要固定下来脚本安装最容易出现的问题反而是“看起来一样实际版本不一样”。我这次直接把版本组合固定在脚本里避免不同节点拉取到不一致的组件组件版本说明Kubernetesv1.29.x控制面和工作节点版本保持一致containerd1.7.x作为 k8s 容器运行时替代 dockerHarborv2.10.x离线包自带镜像和 docker-compose 编排CNI 插件flannel v0.24.x简单、够用内网环境首选资源上我按最小可用配置估算控制面节点 4C/8G工作节点 4C/8GHarbor 单独放一台 4C/8G 的机器数据盘预留 200G。注意 Harbor 如果没有独立数据盘镜像和数据库很快会把系统盘占满这一点后面我会再讲。2.2 初始化脚本里每行配置背后的原因系统初始化脚本是所有节点的第一步它做的不只是“关掉多余的东西”更重要的是让操作系统满足 kubelet 和 containerd 的运行要求。下面是我实际脚本中的核心片段#!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail init_system() { # 关闭 swap。kubelet 在默认配置下对 swap 非常敏感 # 虽然新版支持 swap 运行但生产环境关闭 swap 仍是主流做法。 swapoff -a sed -i / swap / s/^/#/ /etc/fstab # 内网测试环境直接关闭防火墙和 selinux省事。 systemctl stop firewalld || true systemctl disable firewalld || true setenforce 0 || true sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config # 加载内核模块 modprobe overlay modprobe br_netfilter # 让 bridge 流量经过 iptables这是 k8s 网络插件工作的前提 cat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --system # 时间同步证书校验和集群心跳都依赖它 systemctl enable chronyd systemctl start chronyd }每一条配置背后都有对应的问题不关 swapkubelet 经常报failed to run kubelet不开br_netfilterCNI 插件创建的网桥流量没办法被 iptables 规则处理NodePort 和跨节点 Pod 通信就会异常ip_forward不开Pod 出网直接不通。时间不同步的坑更隐蔽kubeadm 生成的证书有效期是基于当前时间的节点间时间差太大apiserver 和 kubelet 之间的 TLS 握手就会出现证书验证失败。这套初始化脚本里还有一个容易被忽略的变量内核参数net.ipv4.conf.*.rp_filter。如果你的机器默认开了严格反向路径过滤并且网络结构比较复杂CNI 插件建的 veth 可能被内核直接丢弃报文。遇到跨节点 ping 不通时先查这一项。3. 控制平面安装脚本kubeadm init 的参数必须提前算好3.1 初始化参数与默认网段冲突控制面脚本的核心是kubeadm init参数看起来不多但每一个都关系到后续网络、证书和高可用。我的脚本里这样写K8S_VERSIONv1.29.6 API_ENDPOINT192.0.2.10 # 主 master 内网 IP 或负载均衡 VIP POD_CIDR10.244.0.0/16 # 必须和你使用的 CNI 默认网段一致 SERVICE_CIDR10.96.0.0/16 IMAGE_REPO你的内网镜像仓库地址 # 或用云厂商的公开加速地址 kubeadm init \ --kubernetes-version${K8S_VERSION} \ --apiserver-advertise-address${API_ENDPOINT} \ --control-plane-endpoint${API_ENDPOINT} \ --pod-network-cidr${POD_CIDR} \ --service-cidr${SERVICE_CIDR} \ --image-repository${IMAGE_REPO} \ --upload-certs这里最需要注意的是--pod-network-cidr。以 flannel 为例它的网络模型默认使用的 Pod 网段就是10.244.0.0/16如果你在 init 时把 Pod 网段写成另一个值CNI 插件下发配置后节点网络就跟声明不一致严重情况下 Pod 一直处于 ContainerCreating。我见过有人随手抄了一个10.32.0.0/16的配置结果换一套网络插件就不通。所以我会先把所有机器内网网段、Pod 网段、Service 网段列在一个表格里脚本通过读同一个配置文件来取值而不是散落在各个脚本里。--image-repository也建议提前决定。如果确实无法直接访问默认镜像源就把参数指到内部同步好的镜像仓库或云服务商提供的公开加速地址。这一步千万不要偷懒否则kubeadm init会在拉取 sandbox 镜像时卡很久最后因为网络超时失败。3.2 从 init 到 CNI 就绪的脚本编排初始化成功后脚本会自动生成工作节点和管理员 kubeconfig。这里要做的关键动作是把 join 命令保存为共享文件同时把管理员配置放到当前用户目录下方便用 kubectl 操作mkdir -p /root/.kube cp -i /etc/kubernetes/admin.conf /root/.kube/config chown $(id -u):$(id -g) /root/.kube/config # 生成供后续节点加入使用的命令 kubeadm token create --print-join-command /root/join-command.txt echo KUBELET_EXTRA_ARGS--node-ip${API_ENDPOINT} /etc/sysconfig/kubelet然后部署网络插件。flannel 的 yaml 是标准内容但我在脚本里做过一次调整因为集群节点不止一台且未来可能要换成其它 CNI所以把 yaml 中的镜像地址做了一层 sed 替换统一走内网镜像仓库。kubectl apply -f kube-flannel.yml # 等待 flannel 相关 pod 就绪 kubectl -n kube-flannel rollout status ds/kube-flannel \ --timeout120s || true等待 Pod 就绪这一步一定要用rollout status或等待循环而不是sleep 30了事。网络插件没起来时kubectl get nodes看到节点都是 NotReady你在上一个阶段跑得过快后面的健康检查就会误判失败。我习惯在所有关键动作之后写一个等待函数比如wait_ready() { for i in $(seq 1 60); do if kubectl get nodes | grep -q Ready ; then break fi sleep 5 done kubectl get nodes }控制面脚本跑完目标状态是kubectl get nodes能看到 master 节点为 Readykube-system和kube-flannel命名空间的核心 Pod 全部 Running。4. 工作节点加入脚本token 过期和重复执行这两个问题绕不开4.1 token、hash 和 certificate-key 如何跨脚本传递工作节点加入集群核心只有一条命令kubeadm join ...。但这条命令没法直接手工粘贴地持续用因为 token 的默认有效期是 24 小时而通过--print-join-command生成的命令里同时包含了 token 和 CA 证书 hash如果主 master 在初始化后长时间没有加入工作节点这条命令会过期。我的处理方式是不要只用一次 join 命令而是在工作节点脚本里加入“重新获取 join 参数”的逻辑。如果join-command.txt存在先判断集群里是否已经有这个节点如果没有允许工作节点脚本通过一个预共享的 token 重新生成 join 参数if [ ! -s /root/join-command.txt ]; then kubeadm token create --print-join-command /root/join-command.txt fi对于多 master 高可用场景--print-join-command生成的命令还要附带--certificate-key这个参数用于解密由--upload-certs上传的控制面证书。默认这个密钥也有一段时间有效期我建议把它和 join 命令写到同一个文件其它控制面节点加入时直接追加--control-plane --certificate-key参数执行。4.2 加入脚本怎么做成幂等“幂等”是脚本化安装比手工安装强得多的地方。工作节点脚本最怕的是你执行到一半发现 join 失败修复后重跑脚本结果 kubelet 已经初始化过了脚本在同一个动作上再次执行就报错。所以我的脚本开头会做三处检查# 1. 检查 kubelet 是否已经在运行 if systemctl is-active --quiet kubelet; then echo kubelet already running, skip join exit 0 fi # 2. 检查是否已经有 kubeconfig 残留 if [ -f /etc/kubernetes/kubelet.conf ]; then echo node already joined exit 0 fi # 3. 确认该节点没有在集群列表里面 if kubectl get nodes | grep -q $(hostname); then echo node exists in cluster exit 0 fi这三项检查有一个逻辑顺序问题如果节点已经加入过但之后删除了节点kubelet 配置还在脚本会认为“已加入”而退出。这时候需要手工清理。所以我在脚本里提供了一个--force-reset参数内部执行kubeadm reset -f rm -rf /etc/kubernetes/manifests /etc/kubernetes/kubelet.conf rm -rf /etc/cni/net.d iptables -F iptables -t nat -F || true ipvsadm --clear 2/dev/null || true注意kubeadm reset -f只是把 kubelet 和集群配置重置不会清理容器运行时里的既有镜像。如果你真的想让这台机器完全回到初始状态还得配合crictl rm --all --force。网上很多“重置集群”教程只讲前半段结果节点 IP 冲突或者端口占用问题依旧在。工作节点 join 时还有一个隐藏参数容易被漏掉--node-ip。在生产服务器有多块网卡的环境里不显式指定 node-ipkubelet 会随机选一个把节点注册到集群之后 apiserver 和 kubelet 之间的连接就非常不稳定。脚本里我给这个参数单独留了变量NODE_IP并且要求部署人员为每台机器填写。5. 独立部署 Harbor离线包加载、证书信任和数据持久化5.1 离线部署 Harbor 的目录与配置Harbor 我选择用官方离线安装包配合 docker-compose 部署而不是 Helm。原因是Harbor 本身要承担“内网镜像源”的角色如果它自身的部署再依赖 k8s 里的另一个仓库会形成鸡生蛋的问题。离线安装包内已经打包好了所有镜像和启动脚本最适合在没有外网的环境里先把它拉起来。下载离线包后我建议先做一步校验把官方提供的 sha256 值和实际文件的摘要做对比脚本里直接写成echo 下载校验和 | sha256sum -c harbor-offline-installer-v2.10.0.tgz.sha256 || exit 1解压后所有配置都集中在harbor.yml。需要重点改的地方有这几个hostname: harbor.local http: port: 80 https: port: 443 certificate: /data/certs/harbor.local.crt private_key: /data/certs/harbor.local.key harbor_admin_password: 改成一个复杂的初始密码 data_volume: /data/harbor/datahostname不是简单的展示字段它最终会写入镜像仓库的访问地址。如果在docker login时用了 IP 或另一个域名就会出现证书和 hostname 不匹配的问题。我这里统一用harbor.local这样一个内部域名并在所有节点的/etc/hosts里加上解析。data_volume也要特别注意。Harbor 的设计里数据库、镜像存储、Redis 数据都在这个目录下。如果系统盘不大建议单独挂一块数据盘比如/data。不然镜像推多了占满根分区之后 Harbor 的 jobservice 和 registry 组件会一起出问题而且很难清理。5.2 证书信任链从自签证书到 containerd 客户端Harbor 默认推荐启用 HTTPS但在内网环境用公开 CA 证书比较麻烦所以常见做法是自签 CA并为harbor.local签发服务证书。自签不是问题问题在于后续每一个使用这个仓库的客户端都要信任这个 CA。我的证书生成脚本很简单# 1. 自签 CA openssl req -new -x509 -days 3650 \ -keyout ca.key -out ca.crt \ -subj /CNHarbor-CA # 2. 签发服务端证书 openssl req -newkey rsa:2048 -nodes \ -keyout harbor.local.key -out harbor.local.csr \ -subj /CNharbor.local openssl x509 -req -in harbor.local.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out harbor.local.crt -days 3650 \ -extfile (printf subjectAltNameDNS:harbor.local,DNS:*.harbor.local)之后需要把这个ca.crt分发到两类地方。一类是 docker 客户端Linux 下放到/etc/docker/certs.d/harbor.local/ca.crt或执行cp ca.crt /usr/local/share/ca-certificates/ update-ca-certificates。另一类是 k8s 节点的 containerd这个会在下一章详细说。我踩过的坑是只分发了证书忘记让 containerd 里的 registry 配置读取它结果crictl pull harbor.local/...一直报x509: certificate signed by unknown authority。后来干脆在联调脚本里把“证书分发”写成强制步骤任何节点加入后先校验证书文件是否存在再继续后续操作。Harbor 安装脚本的执行顺序也要固定先确认 docker-compose 可用再执行./install.sh最后等待服务健康。Harbor 的install.sh会自己调用 prepare 生成配置并启动全部容器。它启动失败的高频原因就两个端口被占用或者harbor.yml解析不过。脚本里检查端口那一步不能省尤其如果机器上已经跑着别的服务占用了 80/443。6. 把 Harbor 配置成 k8s 默认私有仓库的联调脚本6.1 打通 containerd 的 registry 配置现在 k8s 集群有了Harbor 也起来了最后一步是把两者连起来让 k8s 节点能从 Harbor 拉取私有镜像。如果节点用的是 containerd配 docker 的/etc/docker/daemon.json是完全没用的改成配置 containerd 的 TLS 证书目录# /etc/containerd/certs.d/harbor.local/hosts.toml server https://harbor.local [host.https://harbor.local] ca /etc/containerd/certs.d/harbor.local/ca.crt同时还要在 containerd 的默认配置中让它读取这个目录。注意有些安装方式默认没有启用config_path需要加上[plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d改完后不用重启整个 containerd最好执行systemctl restart containerd后再用crictl pull harbor.local/k8s-apps/demo:v1验证一次。这里有一个非常容易踩的细节hosts.toml里server字段需要和镜像仓库地址完全一致如果 hosts.toml 写的server https://192.0.2.30而镜像名用的是harbor.local/k8s-apps/...仍然会走默认的普通 HTTPS 通道证书校验照样失败。把这些领域写在联调脚本里就比人肉排查省事很多。6.2 创建项目、账号与拉取验证Harbor 的项目和账号我建议用脚本创建。比起在网页上点按钮脚本创建的方式更适合复制到另一个环境也方便以后对照检查。Harbor 有完整的 REST API创建项目只需要一个请求curl -u admin:初始密码 -X POST \ https://harbor.local/api/v2.0/projects \ -H Content-Type: application/json \ -d {project_name:k8s-apps,public:true}我一般会把项目设置为公开或者使用独立的 robot 账号。k8s 里拉取镜像最终需要的是docker-registry类型的 Secret如果项目是公开的这个 Secret 可以不写如果项目是私有的机器人账号比普通用户账号更合适因为它的权限可以限定到某个项目而且密码可以旋转。脚本中创建账号并绑定项目后生成 k8s Secret 的命令是kubectl -n default create secret docker-registry registry-secret \ --docker-serverhttps://harbor.local \ --docker-usernamerobot$k8s-apps \ --docker-password机器人账号token最后用一个 Deployment 做全链路验证。比如kind: Deployment apiVersion: apps/v1 metadata: name: harbor-pull-test spec: replicas: 1 template: metadata: labels: app: harbor-pull-test spec: containers: - name: demo image: harbor.local/k8s-apps/demo:v1 imagePullPolicy: IfNotPresent imagePullSecrets: - name: registry-secret验证成功后先查看kubectl get events --all-namespaces有没有Failed to pull image再看crictl ps容器是否正常启动。这一步如果通过基本说明整个 k8s 和 Harbor 的链路已经打通后面就是业务镜像如何推送和配置的问题了。7. 实测中的坑和对应的脚本加固手段7.1 多网卡环境下节点 IP 错乱这个问题我在前面提到过一次但值得单独拿出来讲因为它的表现太有迷惑性kubeadm init 正常join 也显示成功可kubectl get nodes看到的节点 IP 是公网网卡地址或者每过几秒 apiserver 就报连接超时。根因是机器上有多块网卡kubelet 注册节点时无法确定应该上报哪个 IP。脚本加固的方式非常简单给 kubelet 统一指定--node-ipecho KUBELET_EXTRA_ARGS--node-ip${NODE_IP} /etc/sysconfig/kubelet systemctl daemon-reload systemctl restart kubelet同时kubeadm init里也显式指定--apiserver-advertise-address避免 API Server 监听在错误的网卡上。这类问题不写进脚本每次重建环境都会原地踩一次。7.2 cgroup driver 不一致导致 kubelet 起不来Kubernetes 1.28 之后的版本对 cgroup driver 要求更加明确建议 containerd 使用systemdcgroup driverkubelet 也一样。如果 containerd 的配置还是默认的cgroupfskubelet 启动时会直接报一个类似 “misconfiguration: kubelet cgroup driver: systemd but Cgroup driver: cgroupfs” 的错误。在安装脚本里初始化 containerd 后必须马上处理这个配置containerd config default /etc/containerd/config.toml sed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml systemctl restart containerd如果不重启 containerd改配置是没有意义的。我在第一次跑脚本时漏了重启结果 kubelet 照样起不来日志翻了好久才发现问题不在 kubelet 而在 containerd。从此我把“改配置后必须重启服务并检查状态”写成了固定模板执行后立即systemctl is-active containerd不通过就中止脚本。7.3 Harbor 存储暴涨和 GC 策略Harbor 在大量推送和删除镜像后磁盘使用量会持续增长。原因一是 registry 的 layer 文件不会因为删除了 tag 就立刻释放二是 jobservice 和日志组件也在写日志。镜像清理需要手动触发 GC但前提是被删除的 tag 在数据库里已经标记为删除否则 GC 看不到可清理的 layer。我给的脚本化方案分两步。第一步是在推送流程上只保留必要 tag删除不要的镜像后调用 GC APIcurl -u admin:密码 -X POST \ https://harbor.local/api/v2.0/system/gc/schedule \ -H Content-Type: application/json \ -d {schedule:{type:Manual}}第二步是给 Harbor 的 data_volume 所在分区加磁盘告警定期du -sh /data/harbor/data/database /data/harbor/data/registry检查容量趋势。真正的大头永远是 registry 目录如果磁盘短期内被占满先看是不是有人推送了大镜像又反复删除。7.4 踩坑问题对照表现象根因脚本加固kubelet 起不来日志提示 cgroup driver 不一致containerd 未启用 SystemdCgroup初始化脚本强制 sed 修改并重启节点 IP 错乱apiserver 连接不稳定多网卡未指定 node-ip统一注入 KUBELET_EXTRA_ARGScrictl pull 报证书未知颁发者containerd 未信任 Harbor CA证书分发脚本强制校验文件存在Harbor 端口正常但 docker login 失败hostname 与证书域名不匹配harbor.yml 模板中锁定 hostname 变量删除镜像后磁盘不释放registry 未 GC删除后调用 GC API 容量告警8. 离线包、日志与回滚脚本安装工具的最后一公里8.1 离线安装包的准备和校验内网环境装 k8s 最麻烦的是镜像。我常用的做法是在一台能访问境内外镜像仓库的机器上先把需要的镜像全部 pull 下来再用docker save或ctr导出成 tar 包最后带到目标机器上导入。但用脚本自动安装时要有一个“导入”的原子操作检查镜像是否已经存在不存在才导入导入完成后校验数量。比如 containerd 环境的脚本可以这样写ctr -nk8s.io images import k8s-images.tar如果镜像较多导入过程可能会比较长脚本要允许失败重试而不是一失败就退出。我在脚本里给这类操作加了--retry逻辑重试三次每次间隔 5 秒。Harbor 离线包本身也是同样的校验思路先sha256sum再解压。这些看起来很简单但很多手工安装的失败正是因为镜像没同步完整中间缺了一个 layer导致 kubeadm init 中途卡住。8.2 日志规范与失败重试机制脚本化安装还有一个容易被忽视的打磨点日志。我要求每个脚本至少做到两件事。第一所有输出统一追加到logs/install-YYYYmmddHHMMSS.log同时仍然让关键状态输出到终端方便人实时观察第二给关键函数加上时间戳标记log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* | tee -a ${LOG_FILE} }失败重试不只是网络操作需要。系统初始化阶段执行sysctl --system后部分云环境可能会有覆盖内核参数的提示这本身不是错误但脚本如果用set -e加持会直接把脚本终止掉导致后面的步骤永远无法执行。我的经验是对这类“可忽略”的错误统一捕获只在真正影响后续动作的命令上严格退出比如kubeadm init失败、systemctl restart containerd失败。判断标准很简单如果这个动作失败后重跑脚本也无法恢复那它就必须让脚本停止。回滚方面k8s 侧用一条kubeadm reset -f配合清理 CNI 配置即可Harbor 侧就比较粗放直接停掉 compose 服务、把 data_volume 目录改名备份然后重新执行安装脚本。因为 Harbor 的配置基本都是声明式的回滚成本明显低于 k8s 集群。所以在脚本设计上我让 Harbor 脚本天然支持“重新安装”而不是依赖一个复杂的 rollback 逻辑。写到这里我把这次 k8s 和 harbor 脚本安装的整个思路和命令都列完了。个人感受是脚本化的真正收益不只是“快”它逼着我把每一步的原理都查清楚否则脚本里任何一个参数错误重跑多少次都是白费。最后再分享一个小建议不要照着别人的脚本直接复制而是先在自己的一台机器上手工跑通一遍把关键动作和报错都记录下来再把这些记录翻译成脚本。这样维护起来的脚本才能在未来某一天环境出问题时真正帮你定位问题所在。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →