简介:这是一套面向运维人员与Kubernetes初学者的集群快速部署脚本包,用于解决多节点环境下Docker、cri-dockerd与k8s组件手动安装配置繁琐的问题。包内共7个文件,以3个sh脚本为主,配合yml、yaml配置、txt教程说明及rpm离线安装包,压缩包约10.67MB,覆盖安装、卸载、网络插件与kubeadm初始化等环节。使用者可按实际集群节点规划调整脚本参数与软件版本,将文件上传至Master和Node的/home目录并执行安装脚本,即可完成集群环境搭建。资源内置Docker 24.0.7、cri-dockerd 0.3.9与Kubernetes v1.28.2的版本组合,便于快速复现实验环境、验证部署流程并积累排错思路。目前已有953人学习下载,适合需要缩短搭建周期、减少重复配置的中级运维读者参考。
1. 从一条命令到一套集群:这份脚本到底替你扛了哪几步
真正耗时间的从来不是kubeadm init那一分钟,而是它之前那二十几步:关 swap、载内核模块、改 cgroup driver、装 cri-dockerd、锁版本、预载镜像、算 token、等 CNI 起来。Docker 容器化 K8s 集群部署这件事,难点不在 K8s 本身,而在三台机器底座是否完全一致——只要有一台的ip_forward是 0,你就会在凌晨两点对着NotReady发呆。这份一键部署 shell 脚本干的就是把底座固化:变量区改六个值,剩下交给函数,重复执行不会把集群搞坏。它适合两类人:需要给团队反复重建测试集群的,以及第一次搭集群又不想在 sysctl 上耗两个晚上的。它不承诺生产级高可用,但会把“能跑起来”到“跑得稳”之间的坑提前标在脚本注释里。
2. 底座先立住:Docker 运行时、内核参数与节点规划的三道关
集群装不上,九成不是 K8s 的问题,是脚本跑之前机器就没准备好。这一章先把三件事说透:运行时为什么选 Docker、内核参数到底在管哪条流量、节点和端口怎么规划才不会在 join 阶段返工。脚本里preflight()函数做的事就是把这三件事变成可判定的检查项,而不是靠人肉记忆。
2.1 为什么这份脚本还要保留 Docker 运行时
K8s 从 1.24 起移除了 dockershim,containerd 成为默认 CRI,这是常识。但很多团队的镜像构建流水线、排障习惯、离线包分发全都长在 Docker 上,docker images、docker save、docker load这套动作比crictl顺手得多。保留 Docker 作为运行时不是不行,代价是要多装一个 cri-dockerd 做 shim,把 Docker 的 API 翻译成 CRI 协议。选它还是选 containerd,我一般按这张表判断:
| 判断项 | Docker + cri-dockerd | 直接用 containerd |
|---|---|---|
| 已有构建流水线 | 复用docker build,零改动 | 需要迁到 buildkit 或 nerdctl |
| 多一个故障点 | 是,cri-dockerd 服务挂了节点就 NotReady | 否,链路短一跳 |
| 离线镜像分发 | docker save/load最直观 | ctr images import,参数略绕 |
| 资源开销 | 多一个 dockerd 常驻 | 更轻 |
| 排障熟悉度 | 大多数人有肌肉记忆 | crictl需要适应 |
如果集群是给业务长期跑生产的,我倾向 containerd;如果这套集群的定位是测试环境、教学环境、或者要和现有 CI 共用镜像,Docker 路线的综合成本更低。脚本用RUNTIME=docker这个变量控制分支,两种都留了口子。
不管选哪个,daemon.json里有一行必须写死,写不对后面一定翻车:
{ "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "storage-driver": "overlay2", "insecure-registries": ["registry.local:5000"] }exec-opts那行是核心——Docker 默认用 cgroupfs,而 kubelet 在 systemd 系统上默认认 systemd,两者不一致时 kubelet 会反复报failed to run Kubelet: misconfiguration: kubelet cgroup driver "systemd" is different from docker cgroup driver "cgroupfs"。日志文件大小上限也得设,不然跑几个月/var/lib/docker/containers能把磁盘吃干净,这种事故我见过两次。insecure-registries按需,用内网仓库时才填。
2.2 内核模块与 sysctl:bridge-nf-call-iptables 到底管什么
这一节是整份脚本里最容易被跳过、也最容易出事的地方。br_netfilter模块不加载,net.bridge.bridge-nf-call-iptables这个 sysctl 写进去也是摆设;写了但值是 0,桥接的 Pod 流量就不会走 iptables 规则链,表现出来的现象是同节点 Pod 访问 Service 的 ClusterIP 超时,而直接访问 Pod IP 又是通的——这个现象非常容易把人带到 CNI 的坑里去。
# 加载容器与网桥过滤所需的内核模块,并做开机持久化 modprobe overlay modprobe br_netfilter cat >/etc/modules-load.d/k8s.conf <<'EOF' overlay br_netfilter EOF # 这三行是集群互通的地基,缺一行都可能出现跨节点不通 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 sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptablesmodules-load.d下的文件保证重启后模块自动加载,只modprobe一次是临时的,机器一重启就回到原点。ip_forward=1控制的是跨节点转发,值为 0 时节点间流量直接丢弃,kubectl get pods会看到大量CrashLoopBackOff。最后那行sysctl是自检,脚本里应该把输出打印出来而不是静默通过——我吃过“以为生效了”的亏,--system在某些精简镜像里会因为文件权限不执行。
如果 kube-proxy 走 IPVS 模式,还得额外加载一批模块,比如ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack。少写一个,kube-proxy 的 Pod 就会在CrashLoopBackOff里循环,日志里写着can't set sysctl或者IPVS proxier will not be used。
2.3 节点 IP、主机名与端口规划表
脚本能不能一把过,取决于变量区填得对不对。最容易忽略的是主机名——kubelet 默认用 hostname 向 apiserver 注册节点,两台机器如果主机名相同,后 join 的会把前一个节点信息覆盖掉,kubectl get nodes里永远只有一个节点,非常玄学。所以脚本第一件事应该是hostnamectl set-hostname或者校验 hostname 唯一。
节点规划我一般按这张表填:
| 角色 | 主机名 | 示例 IP | 配置建议 |
|---|---|---|---|
| 控制面 | node-master-01 | 10.0.10.11 | 2C4G 起,etcd 和 apiserver 同机 |
| 工作节点 1 | node-worker-01 | 10.0.10.21 | 4C8G,按 Pod 密度调 |
| 工作节点 2 | node-worker-02 | 10.0.10.22 | 同上 |
| Pod 网段 | — | 10.244.0.0/16 | 与节点网段、Service 网段不重叠 |
| Service 网段 | — | 10.96.0.0/12 | kubeadm 默认值,一般不用改 |
网段重叠是最隐蔽的一类问题:节点网段是10.244.x.x,又把--pod-network-cidr设成10.244.0.0/16,结果 Pod 路由和宿主机路由打架,排查时看路由表能看半天。端口方面,脚本至少要保证6443(apiserver)、10250(kubelet)、2379-2380(etcd)、8472/udp(flannel vxlan)、179(calico BGP)是通的。如果机器上有防火墙,firewall-cmd --add-port要提前放行,或者干脆在预检阶段直接停掉——测试环境我一般选择关掉,省下的时间够写三个业务模块。
3. 拆开脚本看流程:四个阶段与它们各自的判断条件
一键脚本之所以敢叫“一键”,靠的不是顺序执行,而是每一步都有幂等判断:已经装过的跳过,已经 init 过的换 join,镜像已存在的跳过 load。这一章按脚本执行顺序拆四个阶段,每个阶段给出关键片段和判断逻辑,你可以对着自己的脚本逐段核对。
3.1 阶段一:环境自检与幂等开关
自检阶段要做的事比想象中多:root 权限、操作系统版本、swap 状态、SELinux、时间同步、端口占用、集群是否已存在。判断集群是否已存在,最简单的标志是/etc/kubernetes/admin.conf是否可读——有它说明这台机器已经 init 过,再跑一次kubeadm init会直接报错退出。
#!/usr/bin/env bash set -euo pipefail # 所有日志统一带时间戳,方便出问题时回溯 log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; } check_root() { [[ $EUID -eq 0 ]] || { log "需要 root 权限"; exit 1; } } disable_swap() { # swap 开着时 kubelet 会拒绝启动,这里做临时关闭加持久化 swapoff -a sed -ri '/\sswap\s/s/^/#/' /etc/fstab log "swap 已关闭并写入 fstab" } check_cluster_exists() { if [[ -f /etc/kubernetes/admin.conf ]]; then log "检测到已初始化的集群,跳过 init 阶段" return 0 fi return 1 }set -euo pipefail是这类脚本的安全带:出错立即退出、引用未定义变量报错、管道中任一环节失败都算失败。没有这三个选项的脚本,某一步失败后它会继续往下跑,最后报一堆无关错误,等于把黑匣子埋了。sed -ri那行把/etc/fstab里的 swap 注释掉,-i是原地改,-r开扩展正则;如果只swapoff -a不写 fstab,重启后 swap 回来,kubelet 又起不来。这段代码里check_cluster_exists返回 0/1,主流程用它决定走 init 分支还是 join 分支,这就是幂等的第一层。
时间同步也得在这一阶段检查。chronyc tracking或者timedatectl一看就知道,节点间时间差超过几秒,证书校验和 etcd 选举都会出幺蛾子。
3.2 阶段二:运行时安装与离线镜像导入
运行时安装的关键词是“版本锁”。kubeadm、kubelet、kubectl 三个包版本必须一致,而且 kubelet 版本不能高于 apiserver 太多(一般不超过一个小版本)。装完立刻apt-mark hold或yum versionlock,不然某次apt upgrade就能把集群升挂。
K8S_VERSION="1.28.4" RUNTIME="docker" install_runtime() { if [[ "$RUNTIME" == "docker" ]]; then # 只装指定大版本,避免源里最新版把 cgroup 行为改掉 apt-get install -y docker-ce=5:24.0.* docker-ce-cli=5:24.0.* containerd.io # cri-dockerd 是 Docker 与 CRI 之间的桥,缺了 kubelet 连不上 dpkg -i ./pkgs/cri-dockerd_0.3.4.3-0.ubuntu-jammy_amd64.deb systemctl enable --now docker cri-docker fi } install_kube_tools() { apt-get install -y \ kubeadm="${K8S_VERSION}-1.1" \ kubelet="${K8S_VERSION}-1.1" \ kubectl="${K8S_VERSION}-1.1" # 锁定版本,防止后续无人值守升级 apt-mark hold kubeadm kubelet kubectl } load_images() { local dir="$1" for img in "${dir}"/*.tar; do [[ -e "$img" ]] || { log "镜像目录为空"; return 0; } # 先查再 load,重复执行时能省掉几分钟 local name name=$(tar -xOf "$img" manifest.json | grep -o '"RepoTags":\[[^]]*' | head -1) log "载入镜像 ${name}" docker load -i "$img" done }cri-dockerd那行的版本号要和你RUNTIME对应,装完必须systemctl enable --now cri-docker,这个服务不开,kubelet 连 socket 时会报failed to get container info。load_images里先解析manifest.json再决定是否 load,是一种粗略的幂等——更严格的做法是拿docker images --format '{{.Repository}}:{{.Tag}}'和kubeadm config images list的输出做diff,差集非空才导。离线环境里这一点特别值钱,因为镜像包动辄几个 G,每次全量 load 会让人怀疑人生。
3.3 阶段三:kubeadm init 与 token 的生成、续期
init 是整份脚本里参数最多的一步,也是最不该写死的一步。用配置文件比堆命令行参数好维护,kubeadm config print init-defaults打一份模板,改advertiseAddress、podSubnet、criSocket三个字段就够了。
# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 10.0.10.11 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock name: node-master-01 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.4 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12criSocket是 Docker 路线特有的坑点:kubeadm 默认探测 containerd 的 socket,你要是不显式指定unix:///var/run/cri-dockerd.sock,它会报“找不到 CRI 端点”然后停在那儿。advertiseAddress必须是其它节点能访问到的内网 IP,填127.0.0.1的话 join 阶段会一直超时。podSubnet要和后面装的 CNI 对得上,flannel 默认10.244.0.0/16,calico 默认192.168.0.0/16,两边不一致时节点NotReady是必然结果。
init 成功后,脚本立刻做两件事:拷 kubeconfig 到当前用户目录,以及生成 join 命令并落到/root/join.sh。token 默认 24 小时过期,过期后 join 会报bootstrap token is expired,所以脚本里应该提供--renew参数,调kubeadm token create --print-join-command重新生成一条,而不是让使用者自己翻文档。
gen_join_cmd() { # 重新生成 token 并拼上 cri-socket 参数,输出到 join.sh kubeadm token create --print-join-command \ | sed 's/$/ --cri-socket unix:\/\/\/var\/run\/cri-dockerd.sock/' \ > /root/join.sh chmod +x /root/join.sh log "join 命令已写入 /root/join.sh" }sed那步是给 Docker 路线补 socket 参数,kubeadm 打印出来的 join 命令不带它,直接复制粘贴到 worker 上会失败。这个细节至少绊过我三次。
3.4 阶段四:CNI 落地与节点 join 的收尾校验
CNI 是最后一块拼图,也是“看起来装完了但没装完”的分水岭。kubectl apply -f一份 CNI 清单,等kube-system下的 Pod 全Running,节点才会从NotReady翻成Ready。这个等待要用轮询而不是sleep 30。
wait_cni_ready() { local timeout=${1:-180} local elapsed=0 while (( elapsed < timeout )); do local not_ready not_ready=$(kubectl get nodes --no-headers | grep -c 'NotReady' || true) if [[ "$not_ready" -eq 0 ]]; then log "所有节点已 Ready" return 0 fi sleep 5 elapsed=$((elapsed + 5)) done log "等待超时,请检查 CNI Pod 状态" kubectl get pods -n kube-system -o wide return 1 }超时后自动打印kube-system下的 Pod 列表,是脚本里性价比最高的一个设计——大部分问题看一眼哪个 Pod 没起来就有方向了。--no-headers去掉表头,grep -c计数,|| true防止grep没匹配时被set -e直接干掉脚本。这几个小细节决定脚本是能自己报错,还是无声无息地卡住。
4. 变量表怎么改:六个必改项、优先级与覆盖规则
拿到脚本第一件事不是执行,是打开变量区看一眼。这一章把必改项、可选项、覆盖规则讲清楚,照着改完基本可以一次跑通。
4.1 六个必须改的变量
这六个值填错,脚本跑完集群也是残的:
| 变量 | 含义 | 填错的表现 |
|---|---|---|
MASTER_IP | 控制面内网地址 | join 超时、apiserver 无法访问 |
NODE_IPS | 工作节点地址列表 | 脚本跳过某台机器或 ssh 失败 |
K8S_VERSION | kubeadm/kubelet 版本 | 与离线镜像 tag 不匹配,拉取失败 |
POD_CIDR | Pod 网段 | 与 CNI 默认值冲突,节点 NotReady |
RUNTIME | docker 或 containerd | socket 路径错误,kubelet 起不来 |
IMAGE_DIR | 离线镜像目录 | 导入为空,ImagePullBackOff |
NODE_IPS建议用数组写,脚本里for ip in "${NODE_IPS[@]}"遍历,比空格分隔的字符串好处理边界。K8S_VERSION要和kubeadm config images list的输出对得上,比如v1.28.4对应kube-apiserver:v1.28.4,版本号差一个补丁号,镜像就得重新拉。
4.2 可选开关与默认行为的取舍
除了必改项,还有几个开关默认值值得掰扯:
OFFLINE=true:离线模式,跳过所有外网访问,全部走本地 tar 包。内网环境必开。INSTALL_CNI=flannel:换成calico时要同步改POD_CIDR,两者默认网段不同。CLOSE_FIREWALL=true:测试环境的默认值。生产环境应该改成false并自己写放行规则。ENABLE_IPVS=true:Pod 数量多时 IPVS 比 iptables 性能好,但要求内核模块齐全。SET_HOSTNAME=true:自动按节点前缀+序号重命名,省去手工改。
默认值我一般只动CLOSE_FIREWALL和ENABLE_IPVS,其它保持原样。开关太多反而增加认知负担,一份脚本能覆盖 80% 的场景就够了。
4.3 配置文件、环境变量与命令行参数的优先级
脚本通常支持三种传参方式:改脚本顶部的变量、用环境变量覆盖、命令行传参。优先级从高到低是命令行 > 环境变量 > 脚本默认值。实现上就是这几行:
# 脚本内默认值 MASTER_IP="${MASTER_IP:-10.0.10.11}" POD_CIDR="${POD_CIDR:-10.244.0.0/16}" RUNTIME="${RUNTIME:-docker}" # 解析命令行参数,覆盖环境变量 while [[ $# -gt 0 ]]; do case "$1" in --master-ip) MASTER_IP="$2"; shift 2 ;; --pod-cidr) POD_CIDR="$2"; shift 2 ;; --runtime) RUNTIME="$2"; shift 2 ;; --renew) RENEW_TOKEN=true; shift ;; *) log "未知参数: $1"; exit 1 ;; esac done${VAR:-default}这套写法让环境变量天然具备覆盖能力,case分支再叠一层命令行。关键是所有参数解析必须在任何实际动作之前完成,别出现“前半段用默认值跑了,后半段才读到用户传的值”这种半截配置。shift 2是因为带值参数占了两个位置,漏写会死循环。
5. 避坑与排查:五条血泪记录的现象、原因与解法
脚本能跑通不代表集群能用。这一章是我在不同机器上踩出来的五条记录,按“现象 → 原因 → 解决”写,遇到相似症状可以直接对号入座。
5.1 网络与 CNI 类故障
第一条:所有节点NotReady,CNI Pod 全在CrashLoopBackOff。
现象是kubectl get nodes全是NotReady,kubectl get pods -n kube-system里 flannel 或 calico 反复重启,日志里出现failed to set up mount或者cannot find network namespace。原因通常是 CNI 的 DaemonSet 没拿到宿主机路径挂载权限,或者节点上残留了上一次部署的/var/lib/cni、/etc/cni/net.d脏数据。解决方式是先清空这两个目录再重装 CNI,命令是rm -rf /etc/cni/net.d/* /var/lib/cni/*,然后删掉 DaemonSet 重新 apply。如果是重装过集群的机器,这一步几乎是必做项。
第二条:跨节点 Pod 互相 ping 不通,同节点正常。
现象很典型:同一台机器上的两个 Pod 能通,跨节点就不行,kubectl exec进去ping一直超时。原因之一是宿主机防火墙拦了 VXLAN 的8472/udp,或者云环境的网络策略把 UDP 封了。另一个原因是节点网卡不是同一网段,路由表里没有对方的回程路由。解决方式是firewall-cmd --add-port=8472/udp --permanent后 reload,或者在脚本里直接把CLOSE_FIREWALL设成true。云上环境还得确认安全组放行,这一步脚本管不了,只能在文档里写清楚。
5.2 运行时与 cgroup 类故障
第三条:kubelet 日志刷屏kubelet cgroup driver "systemd" is different from docker cgroup driver "cgroupfs"。
现象是 kubelet 反复重启,journalctl -u kubelet里满屏都是这行。原因就是 2.1 节说的,Docker 和 kubelet 的 cgroup driver 不一致。解决办法是先确认/etc/docker/daemon.json里有"exec-opts": ["native.cgroupdriver=systemd"],然后systemctl daemon-reload && systemctl restart docker。注意顺序:必须重启 docker 再重启 kubelet,反过来不生效。改完docker info | grep Cgroup应该显示systemd,这一步验证不能省。
第四条:kubeadm init卡在Waiting for the kubelet to boot up the control plane。
这个阶段超时通常是两件事:要么 kubelet 根本没起来(版本不匹配、配置错),要么镜像没拉到。先看journalctl -u kubelet -n 50,如果日志里有failed to pull image,就是离线镜像没导全;如果什么都没有,可能是kubelet服务被 mask 了或者配置文件路径不对。解决方式是kubeadm config images list --kubernetes-version v1.28.4打一份期望清单,和docker images的实际清单做 diff,差什么补什么。
5.3 证书、token 与时间类故障
第五条:第二天加节点,join 报bootstrap token is expired。
kubeadm init生成的 token 默认 24 小时过期,隔天再 join 必挂。原因不是集群坏了,是 token 有效期到了。解决方式是主节点执行kubeadm token create --print-join-command,重新生成一条带新 token 的命令,别忘了补--cri-socket参数。如果是证书过期(默认一年),用kubeadm certs check-expiration查看剩余时间,kubeadm certs renew all续期后重启 control plane 的静态 Pod。这类问题最好的处理方式是写进脚本的--renew分支,而不是等使用者撞上再去翻文档。
6. 验收与进阶:把一键部署变成可重复交付
脚本跑完、节点全Ready,只能算交付了一半。真正的验收要能证明“集群可以跑业务”,而不只是“进程活过来了”。
6.1 三类验收脚本
第一类是节点与组件状态:kubectl get nodes -o wide、kubectl get pods -n kube-system,重点看有没有CrashLoopBackOff和重启次数非零的 Pod。第二类是网络连通性:起一个测试 Deployment 扩到两个副本,用kubectl expose建一个 ClusterIP Service,再在另一个 Pod 里curl服务名,能通说明 DNS、kube-proxy、CNI 三层都活着。第三类是调度与存储:故意起一个带nodeSelector的 Pod,确认它能落到指定节点;如果接了存储类,再跑一次 PVC 绑定,确认Pending能在几十秒内变成Bound。
# 一次跑完三类检查,输出直接贴到交付记录里 kubectl get nodes -o wide kubectl get pods -n kube-system | awk 'NR==1 || $4!="Running" || $5!=0' kubectl create deployment verify --image=registry.local:5000/busybox:1.36 --replicas=2 -- sleep 3600 kubectl expose deployment verify --port=80 --target-port=80 kubectl run curl-test --rm -it --image=registry.local:5000/curl:latest --restart=Never -- \ curl -sS --max-time 5 verify.default.svc.cluster.local kubectl delete deployment verify && kubectl delete svc verifyawk那行是过滤异常 Pod 的偷懒写法,$4是 STATUS 列,$5是 RESTARTS 列,只要有不等于Running或者重启次数不为 0 的就打出来。这个过滤器比肉眼看几十行舒服得多,交付前跑一遍基本能兜住八成问题。
6.2 加一个本地仓库,让第二次部署快一半
第一次部署最慢的环节永远是镜像分发。三台机器各自docker load几十个镜像包,磁盘 IO 和网络都没有并行起来。进阶做法是先在一台机器上起一个轻量 registry,所有镜像推进去,其它节点只 pull 不 load。
# 主节点起一个本地仓库,容器化方式即可 docker run -d -p 5000:5000 --restart=always \ -v /data/registry:/var/lib/registry \ --name registry registry:2 # 把离线镜像批量打上仓库 tag 并推送 for img in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do docker tag "$img" "registry.local:5000/${img}" docker push "registry.local:5000/${img}" doneregistry.local这个域名要提前写进各节点的/etc/hosts,或者把镜像名统一换成IP:5000的形式,否则 pull 时会解析失败。--restart=always保证 registry 容器在机器重启后自动拉起,不然下次部署第一件事就是修仓库。这个改动看起来只是换了分发方式,实际能把重复部署的时间压掉一半以上,尤其节点数超过三台时差别非常明显。
另外一个值得加进脚本的小动作是部署前后各打一次kubectl get events -A --sort-by=.lastTimestamp的尾部若干行,存成日志文件。出问题时不用从零开始回忆,出问题前十几分钟的调度事件、拉镜像失败、探针失败都记在里面。
从那以后我每次重建集群,都会先拿kubeadm config images list的输出和本地镜像目录做一次 diff,确认零差异再执行脚本,宁可多花两分钟也不赌“应该能拉到”。希望帮到你。
本文还有配套的精品资源,点击获取