资讯动态

Ubuntu上Kubernetes 1.23集群部署完整实战指南

发布时间:2026/10/22 10:22:26 来源:尧图企业网站定制
从 1.23 上车的 Kubernetes在 Ubuntu 上完整部署实录做集群部署的这些年我有个习惯每次 Kubernetes 发布新版本后都不会急着跟风升级而是先等社区跑一段时间看看有没有共性问题。1.23 这个版本在我看来是比较特殊的一个它是破天荒支持弃用 Docker 作为运行时之前还保留着很多传统运维习惯的版本也是很多企业生产环境至今仍在使用的主流版本。我自己在 Ubuntu 20.04 上从零部署过十几套 1.23 集群有测试环境也有实际承载业务的过程中踩了不少坑今天把这套流程完整写出来希望给正准备上车的朋友一个参考。这篇文章适合谁适合那些已经会用 Linux、理解容器基本概念但还没真正动手搭过 Kubernetes 集群的工程师。我会从环境准备开始一路走到工作节点加入集群并把常见故障的排查思路也放进来。我会尽量把每一步背后的原因说清楚而不是只给命令。1. 为什么是 1.23在 1.24 移除 Docker 支持之前的一个重要版本很多人会问为什么偏偏选 1.23现在都有更新的版本了。这里有个历史背景Kubernetes 在 1.20 版本正式宣布弃用 dockershim也就是让 kubelet 直接管理 Docker 容器的组件1.23 是 dockershim 仍然可用的最后一个版本到 1.24 就彻底移除了。虽然对大部分使用 containerd 的用户来说Docker 是否被直接支持影响不大但 1.23 毕竟是一个分水岭——之前的部署习惯、网络插件的兼容性、各种周边工具的状态在这个版本上都是最成熟的。从稳定性角度讲1.23 属于 1.x 系列中比较皮实的版本。它的 API 还保留了不少旧版本字段如果你手里有早期编写的资源配置清单迁移成本比较低。还有一个隐性好处很多云厂商的托管 Kubernetes 服务在 1.23 时代经历了大规模验证互联网上能搜到的踩坑案例也最丰富真出问题的时候更容易找到参考。我个人的建议是如果你不是因为特殊兼容性要求必须用旧版本1.23 也是一个非常值得练习和学习的版本。它不像 1.20 那么古老很多新特性已经引入又不至于像 1.28 之后那样某些配置方式变得过于抽象对新手不友好。在你完整掌握 1.23 之后再往上升级逻辑上会更顺。2. 环境准备操作系统、网络、时间的三个前置坑2.1 操作系统版本与内核参数首先明确基础镜像。我比较推荐 Ubuntu 20.04 LTS原因很简单生命周期长、内核版本合适默认 5.4与 Kubernetes 1.23 官方支持列表完全匹配。如果你手边是 Ubuntu 22.04也不是不行但内核变成了 5.15个别驱动和模块需要额外确认我后面会提到。不要用太小众的衍生版本否则出了问题很难判断是你自己配置的问题还是系统底层的问题。安装好系统之后第一件事是确认主机名和 hosts 解析这步很多人会忽略。我习惯把几台机器的 hostname 设置成有规律的名字比如 k8s-master、k8s-node-1、k8s-node-2然后在每台机器的/etc/hosts里把所有机器的 IP 和主机名都写进去。因为 Kubernetes 组件之间通信非常依赖主机名互访尤其是 kubelet 向 apiserver 注册节点时hostname 不对或者解析不了会出现各种奇怪的连不上问题。然后是内核参数。Kubernetes 要求网桥过滤和端口转发行为符合预期需要加载br_netfilter模块并把下面几个参数写入/etc/sysctl.d/k8s.confcat EOF /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --system执行完可以检查一下lsmod | grep br_netfilter sysctl net.bridge.bridge-nf-call-iptables如果grep没有输出说明模块没加载手动执行modprobe br_netfilter再查。这个模块不加载的后果很直接集群内的 Pod 之间通信会出现随机超时特别是使用 Overlay 网络时尤其明显。2.2 网络规划与防火墙部署前就要想好网段规划。Kubernetes 会涉及两个虚拟网段一个是 Service 网段一个是 Pod 网段。它们不能和宿主机物理网络重叠。举个例子如果服务器所在局域网是 192.168.1.0/24你再把 Pod 网段设成 192.168.0.0/16不冲突可以。但如果你把 Service 网段设成 192.168.1.0/24那问题就大了Kubernetes 会把原本访问网关的流量也当作集群内部流量处理网络立刻瘫痪。我这套环境用的规划是Service 网段10.96.0.0/12Pod 网段10.244.0.0/16Ubuntu 默认不带图形化防火墙但有可能启用了 ufw。建议先确认一下生产环境你可以根据自己的安全策略放行但测试环境我建议直接关闭避免干扰。Kubernetes 在初始化时如果发现某些端口被阻会卡在某个节点注册阶段报错又不会直接提示防火墙问题排查起来很痛苦。需要放行的关键端口我列在下面如果之后接入了外部负载均衡器再看情况调整。方向端口用途节点间6443kube-apiserver节点间2379-2380etcd节点间10250kubelet节点间10251kube-scheduler节点间10252kube-controller-manager节点间10255kubelet 只读端口旧版本2.3 时间同步和软件源时间不同步是 Kubernetes 集群最隐蔽的隐患之一。etcd 对时间偏差非常敏感默认情况下如果节点间时间差超过 1 秒就会出现 leader 频繁切换或者请求失败。我在部署每台机器前都会执行apt install -y chrony systemctl enable --now chrony timedatectl set-ntp true然后检查chronyc sources -v确保能看到^*开头的同步源。软件源建议换成国内的镜像源这样后续安装软件包会快很多。修改/etc/apt/sources.list的时候注意Ubuntu 20.04 用的是focal版本代号别写成其他版本的。我一般直接用阿里云或者中科大源选一个固定下来就行不用来回换。3. 容器运行时准备搭载 Containerd 的细节3.1 为什么弃用 Docker 而用 Containerd在 1.24 之前你仍然可以把 Docker 作为 Kubernetes 的运行时前提是 kubelet 通过 dockershim 与 Docker 通信。但 dockershim 本身是个临时组件维护质量不高官方也一直建议大家用 containerd。而且 containerd 的镜像管理、日志采集、性能表现都比 Docker 更轻量kubelet 可以直接通过 CRI容器运行时接口与 containerd 通信少一跳故障点就更少。我强烈建议从一开始就使用 containerd。即使你之前习惯用 Docker 的docker ps查看容器在 Kubernetes 场景下你更频繁使用的是crictl、ctr这些工具。早切换早适应。3.2 安装并配置 ContainerdUbuntu 20.04 自带的 apt 源里就有 containerd但版本比较旧和 Kubernetes 1.23 配合时可能会有兼容问题。我习惯从官方源安装最新稳定版。先把依赖装上apt install -y containerd但这里有个坑apt 安装完的 containerd默认配置里没有启用 CRI 插件。你需要手动生成完整配置containerd config default /etc/containerd/config.toml然后修改两处关键内容。第一处是SystemdCgroup把SystemdCgroup false改成SystemdCgroup true。原因是 Ubuntu 的 init 系统是 systemd容器需要匹配 systemd 的 cgroup driver否则 kubelet 会报 cgroup 驱动不一致的错误节点状态会一直是 NotReady。第二处是 sandbox 镜像地址。containerd 默认的 pause 镜像地址是registry.k8s.io/pause:3.6这个地址在国内访问可能不通。你需要把它替换成可访问的地址比如[plugins.io.containerd.grpc.v1.cri] sandbox_image registry.aliyuncs.com/google_containers/pause:3.6改完后重启systemctl restart containerd systemctl enable containerd然后用crictl验证。如果还没装 crictl可以先装apt install -y cri-tools crictl version正常能看到RuntimeName: containerd的相关输出。3.3 Cgroup 驱动的选择为什么不能只管一致初学者最容易出现的问题是在 kubelet 里设置的 cgroup 驱动和容器运行时不一致。kubeadm 初始化时可以指定--cri-socket和节点配置默认情况下 kubelet 的 cgroup 驱动是systemd而 containerd 默认是cgroupfs。两者不一致时kubelet 会把节点标记为 Ready 但 Pod 无法创建或者直接报错。还有一种情况是 systemd 本身管理 cgroup 的规则与 cgroupfs 冲突导致系统负载过高。我的处理方式是统一使用 systemd 驱动毕竟在 systemd 系统上这是最稳的。containerd 配置里已经改过了kubelet 这边只需要在初始化参数中显式声明--node-ip10.0.0.10 --pod-infra-container-imageregistry.aliyuncs.com/google_containers/pause:3.6node-ip这个参数也很重要尤其是在多网卡机器上。如果你的服务器有内网 IP 和外网 IP不指定 node-ip 的话kubelet 会拿默认路由对应的 IP可能会把外网 IP 注册到集群里导致别的节点访问不了。4. 安装 kubeadm、kubelet、kubectl 的版本锁定4.1 添加 Kubernetes 软件源安装这几个工具最重要的是版本指定。不能直接apt install kubelet因为那样会装最新版本和我们的 1.23 集群不匹配。我的做法是先添加 Kubernetes 官方 apt 源然后列出可用版本再精确安装。先加签名密钥curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.23/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg再写源文件以 1.23 为例cat EOF /etc/apt/sources.list.d/kubernetes.list deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.23/deb/ / EOF apt update注意这里的源路径是固定的core:/stable:/v1.23不要自己乱改。如果你用的是旧版源方式apt-keyUbuntu 20.04 也支持但毕竟apt-key已经弃用新配置建议用上面这种方式。4.2 指定具体版本安装查看有哪些 1.23 的小版本apt-cache policy kubelet | grep 1.23然后安装版本号要以 apt 源列出的为准比如 1.23.17-00apt install -y kubelet1.23.17-00 kubeadm1.23.17-00 kubectl1.23.17-00这里有个小技巧后面的版本号必须完整匹配不同小版本之间命令用法差异不大但建议所有节点统一版本否则升级或排查时会混乱。安装后立刻执行apt-mark hold kubelet kubeadm kubectl把这三个包锁定防止哪天不小心执行apt upgrade把版本升级到另一个大版本集群就再也起不来了。4.3 使用 kubeadm init 初始化控制平面在控制平面节点上先执行镜像拉取的预检kubeadm config images pull --image-repository registry.aliyuncs.com/google_containers这里指定了阿里云的镜像仓库替代默认的k8s.gcr.io不然大概率拉不下来。如果你的网络环境良好也可以不指定。完成之后执行kubeadm init \ --apiserver-advertise-address10.0.0.10 \ --control-plane-endpoint10.0.0.10:6443 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12 \ --kubernetes-versionv1.23.17 \ --image-repository registry.aliyuncs.com/google_containers参数说明一下--apiserver-advertise-address是控制平面节点对外提供 API 服务的地址我这里用了内网 IP。--control-plane-endpoint如果后面要搭高可用集群这里填负载均衡地址单节点集群就填本机 IP后边加:6443端口。--pod-network-cidr必须与你选择的网络插件需要的网段一致我选的 Calico 默认支持 10.244.0.0/16当然 Calico 也可以自定义我习惯直接用这个值。--kubernetes-version不指定的话kubeadm 会尝试拉取最新稳定版导致与已安装的 kubelet 版本不匹配。初始化过程大约几分钟成功后会输出一段信息包括如何配置 kubectl、如何让工作节点加入集群。这段输出是唯一的 join 凭证记得保存到本地文件最好复制到三处丢了只能重新生成。输出末尾大致长这样Your Kubernetes control-plane has been initialized successfully! ... kubeadm join 10.0.0.10:6443 --token ... \ --discovery-token-ca-cert-hash sha256:...先按提示配置普通用户使用 kubectlmkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后检查节点状态kubectl get nodes此时你会看到控制平面节点的状态是NotReady这是正常的因为还没安装网络插件。5. 网络插件 Calico 的部署与调优5.1 Calico 与网络模式选择网络插件是 Kubernetes 集群的命脉。可选的方案很多Flannel、Calico、Weave、Cilium 等等。我的选择是 Calico理由有几点性能好支持网络策略BGP 模式能适应较大规模的集群。Flannel 配置简单但功能弱Cilium 强但依赖内核 BPF 特性在 Ubuntu 20.04 旧内核上部分功能受限。Calico 是一个折中方案在 1.23 时代成熟度非常高。Calico 有两种工作模式IPIP 模式Overlay和 BGP 模式Underlay。IPIP 模式对底层网络没有特殊要求报文会被封装在隧道里适合局域网环境配置简单BGP 模式要求所有节点能够互相建立 BGP 会话适合跨网段、多机房场景。单集群局域网部署用 IPIP 就够了。5.2 部署 Calico 的完整步骤下载 Calico 对应版本的清单文件。我用的版本是 3.23与 Kubernetes 1.23 兼容性良好curl -O -L https://raw.githubusercontent.com/projectcalico/calico/v3.23.0/manifests/calico.yaml在应用之前需要把清单里的 Pod 网段改成我们初始化时指定的网段。找到calico-ipam这个 ConfigMap里面一般会有类似10.244.0.0/16这样的地址。如果下载的默认配置已经是这个网段那就不用改。然后应用kubectl apply -f calico.yamlCalico 组件会以 DaemonSet 方式在每个节点上运行包括calico-node、calico-kube-controllers。等一分钟后检查 Pod 状态kubectl get pods -n kube-system你会看到coredns-xxxxxxxxxx和calico-node-xxxxx等 Pod 陆续变为 Running。同时节点状态也会变成 Ready。5.3 验证网络功能网络插件部署完成不代表一定没问题。我每次都会做几个最基本的验证kubectl get nodes kubectl get pods -n kube-system kubectl run test-$RANDOM --imagenginx --restartNever --port80 kubectl expose pod test-xxxx --typeNodePort --port80 --nametest-svc kubectl get svc用kubectl get pods -o wide查看测试 Pod 的 IP再去另一个节点上ping这个 IP能通就说明 Pod 网络的路由和封装是正常的。如果 ping 不通检查 Calico 的calico-node日志重点看 BGP peering 是否建立、IP 池是否有冲突。另一件很关键的事是查看 kube-proxy 是否正常。它是 DaemonSet如果某个节点上的 kube-proxy 处于 CrashLoopBackOff那么 Service 的转发就会失效即使 Pod 网络通了通过 Service 访问也会超时。6. 工作节点加入集群从 token 生成到节点就绪6.1 生成新的 join token控制平面初始化成功后会有一个有效期为 24 小时的 token。如果超过时间或者你直接忘了保存也不用重新初始化集群在控制平面节点上重新生成即可kubeadm token create --print-join-command这个命令会输出完整的kubeadm join命令包含一个全新的 token 和 discovery-token-ca-cert-hash。如果你想设置 token 的有效期可以加--ttl0表示永久但生产环境不建议毕竟 token 是集群的入场凭证越短越安全。另外如果你的控制平面节点有多个还需要额外为每个控制平面节点生成 cert 信息这个后续再说单控制平面场景用上述命令就够了。6.2 工作节点的预处理每台工作节点都需要重复前面提到的环境准备装 containerd、装 kubelet/kubeadm/kubectl可以用 apt-mark hold 固定版本、时间同步、sysctl 参数。然后执行控制平面节点输出的 join 命令kubeadm join 10.0.0.10:6443 --token xxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxx如果在 join 之前节点上还有以前的 Kubernetes 残留配置最好先清理干净kubeadm reset -f rm -rf /etc/cni/net.d iptables -F iptables -t nat -F这个不是每次都必须做但如果同一个节点反复加入失败大部分时候是之前的网络配置和kubelet缓存残留导致的。特别是/etc/cni/net.d目录Calico 如果写入了旧的配置新的节点加入时会认为网络已经配置好从而跳过 Pod 网络创建。6.3 加入集群后的检查在控制平面节点上执行kubectl get nodes正常情况下工作节点状态会先变为NotReady然后在 10 秒到 30 秒内变成Ready。如果一直处于NotReady去工作节点上查看 kubelet 状态systemctl status kubelet journalctl -u kubelet -f观察日志里是否有关于 CNI 配置、cgroup 驱动、证书过期之类的关键字。最常见的是container runtime is down说明 kubelet 连不上 containerd检查/var/run/containerd/containerd.sock是否存在以及 containerd 是否启动。等所有节点 Ready 后我用几组简单命令验证集群整体功能kubectl get nodes -o wide kubectl get pods -A kubectl get svc -A再部署一个简单应用测试服务发现创建一个 Deployment暴露成 ClusterIP 服务然后进入另一个 Pod 里用服务名访问。如果通了说明 DNS 和 kube-proxy 都正常。7. 踩坑实录从失败日志到问题定位的完整链路部署 Kubernetes 不可能一次成功下面这几个坑是我自己反复遇到过的写出来能帮你省下大量排查时间。7.1 案例一初始化时卡在等待 kubelet 启动症状kubeadm init执行后卡在[kubelet-start] Waiting for kubelet to start很长时间最后超时失败。排查思路先看 kubelet 为什么没起来。执行journalctl -u kubelet -f systemctl status kubelet我遇到的一次是/var/lib/kubelet/config.yaml里指定的 cgroup-driver 与 containerd 不一致。日志里会出现类似failed to run Kubelet: misconfiguration: kubelet cgroup driver: systemd but systemd cgroup driver is not set的提示。解决办法很简单重新初始化前手动改掉这个小配置文件或者直接用正确的kubeadm init参数比如加--cri-socket/run/containerd/containerd.sock重新来。另一个常见原因是 swap 未关闭。Kubernetes 从 1.23 开始如果检测到 swap 未关闭kubelet 会直接罢工。Ubuntu 默认没开 swap但如果你手动创建的 swap 分区或 swapfile 没关也会出问题。执行swapoff -a sed -i / swap / s/^/#/ /etc/fstab7.2 案例二coredns 一直 Pending症状kubectl get pods -n kube-system里 coredns 一直显示Pending不跑起来。Pending 意味着调度器无法把 Pod 调度到任何节点上。最常见的原因是缺少资源或者节点上有污点无法容忍。检查一下节点资源kubectl describe node node-name如果节点状态是 Ready且没有污点那很可能是 CNI 网络没有就绪导致 Pod 无法分配 IP。注意coredns 在 Calico 还没部署完成时处于 Pending 是正常的因为 kubelet 在所有网络就绪之前会一直等待。等 Calico 的 Pod 变成 Running 后coredns 会自动开始调度。但如果 Calico 本身出现问题比如calico-node一直在 CrashLoopBackOff就需要深入了。我遇到过某台节点内核不支持nf_conntrack_proxy之类的模块Calico 日志会报错。这种属于内核模块问题重新编译内核不现实更好的方案是换网络插件比如 Flannel。Flannel 对内核要求低很多普通环境都能跑。7.3 案例三工作节点加入后一直 NotReady症状join 命令执行成功控制平面也能看到节点但节点状态一直NotReady。我遇到的最典型的根因是 kubelet 无法访问到 apiserver 的地址。检查工作节点上kubelet的启动参数它会把 apiserver 地址写成--kubeconfig /etc/kubernetes/kubelet.conf在这个文件里查看 server 字段grep server /etc/kubernetes/kubelet.conf如果这里的 IP 和控制平面实际 IP 不一致比如你初始化时用了10.0.0.10但工作节点却只能走某个网卡访问10.0.0.10这时要么修正路由要么在kubeadm join时加参数指定。再一个容易忽略的是工作节点的主机名。如果两台机器 hostname 重复Kubernetes 会把它们当成同一个节点后者加入时会直接报错。我必须在每台机器上检查hostname保证全局唯一。还有一种情况是证书哈希不匹配。你复制的 join 命令里--discovery-token-ca-cert-hash是从控制平面拿的但如果有人重新生成过 CA 证书那哈希就变了。重新跑一次kubeadm token create --print-join-command是最稳妥的。7.4 案例四Pod 之间可以 ping 通但 Service 访问超时这个问题在 Debian/Ubuntu 上比较常见。kube-proxy 默认使用 iptables 模式如果主机上开启了反恶意软件或自己写了 NAT 规则可能会导致 kube-proxy 的规则被覆盖。查看 kube-proxy 日志kubectl logs -n kube-system kube-proxy-xxxxx如果日志里有关于error acquiring contract的内容大概率是 nftables 和 iptables 冲突。Ubuntu 20.04 默认使用 nftables 作为防火墙后端但 kube-proxy 1.23 的 iptables 模式是基于 iptables 的两边不能协同工作。解决办法是显式让 kube-proxy 使用 ipvs 模式这个需要在 kubeadm 配置文件中指定。或者把系统自带的防火墙彻底关闭保证 iptables 规则不被外部修改。用 ipvs 模式需要在节点上加载内核模块modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh然后在 kubeadm 的kube-proxy配置中加mode: ipvs。具体操作可以这样初始化前在配置文件中加入proxy: mode: ipvs或者在初始化后通过 ConfigMap 修改随后重启 kube-proxy。实测 ipvs 模式在高并发下稳定性更好排查规则也更直观。8. 部署完成之后的必要检查从集群安全到运行机制集群搭建完成不等于事情结束还有几个容易被忽略的点有些会影响安全有些会影响后续维护。8.1 kubeconfig 与权限隔离控制平面初始化后默认的管理员凭据在/etc/kubernetes/admin.conf这个文件拥有集群最高权限。绝对不要把这个文件放到不安全的位置也不要让普通用户直接拿到。日常操作建议创建最小权限用户比如只授某个 namespace 权限的 ServiceAccount这样即使泄漏损失也可控。另外如果要从外部机器远程管理集群把 admin.conf 拷贝到本地机器时把里面的server地址改成外部可访问的地址。否则你会遇到一个很尴尬的问题本机明明能访问集群换个机器就不行了。8.2 节点资源预留与 Pod 驱逐策略Kubernetes 默认不会为系统进程预留资源极端情况下 Pod 会把节点的 CPU 和内存耗尽导致 SSH 都登不上。我习惯在每台节点的 kubelet 配置里设置system-reserved和eviction-hard。例如在/var/lib/kubelet/config.yaml中systemReserved: cpu: 500m memory: 1Gi evictionHard: memory.available: 500Mi这样 kubelet 会主动驱逐 Pod而不是让节点直接卡死。调完这些参数后需要重启 kubelet并观察节点状态变化确保没有误杀系统服务。8.3 备份与升级思路控制平面和 etcd 的数据备份是个大话题但至少应该知道 etcd 数据目录在哪。在 1.23 中如果使用 kubeadm 部署的 etcd数据目录是/var/lib/etcd。最简单的验证性备份是etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/server.crt \ --key/etc/kubernetes/pki/etcd/server.key \ snapshot save /data/backup/etcd-snapshot.db先验证集群可以正常备份后续再考虑定期自动化。这个动作我在每个集群上线后第一时间执行一次确保备份链路是通的。9. 最后分享一个我实际部署时的小习惯在整套流程跑通之后我通常会把所有安装过程中执行过的命令整理成一个 shell 脚本同时把每个节点的配置文件、证书备份、状态检查结果都记录在一个文档里。这样做的原因很简单Kubernetes 的部署过程可以说是一次性的很难保证将来重装时还能记得每一个细节。有了这些记录下次不管是批量加节点还是整个环境重建都能缩短大半时间。特别是集群版本升级的时候我一般会先在一个临时节点上模拟操作确认无误后再在真实节点上执行。用 kubeadm 升级到更高小版本时官方文档里的步骤很详细但实际操作中每次都会遇到一些意外比如镜像拉不下来、证书过期、某个 API 字段被删除都只能靠同时查看官方变更记录和线上日志来解决。对于刚接触 Kubernetes 的同学我还是建议先把 1.23 这套部署流程完整走一遍不要跳过网络插件和踩坑验证。因为后面很多运维问题本质都出在这两个环节。把基础打牢了再去看更高版本的变化就会觉得一切顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑