• 【k8s实战】kubeasz离线部署多master高可用集群


    背景

    kubeasz是一个利用ansible-playbook基于二进制方式自动化部署和运维k8s集群的开源项目,目前该项目最新发布版本为3.3.0,基于该版本我们可以快速实现部署最高版本为1.24.1的k8s集群。

    安装规划

    K8s HA-architecture

    e94bc26a8e9425ba0e55dc7f9ce30689.png

    基于kubeasz安装的k8s高可用架构方案如上,目前kubeasz的官方文档中Node节点还使用的是haproxy处理自身高可用,但实际上kubeasz-3.3.0目前是使用kube-lb代替了haproxy,这部分内容官方还没有来得及更新,使用上留意下就可以了。

    使用kubeasz默认安装的kubernetes集群主要特性如下

    •etcd集群独立于kubernetes集群

    •kubernetes三大组件均二进制运行

    •各节点服务器都经过基本的性能基线设置,能满足常规的使用场景

    •使用证书有效期为100年

    •集群通过kube-lb组件实现内部高可用

    •集成安装了kubernetes-dashboard,开箱即用

    高可用集群所需节点配置如下

    角色

    服务器

    描述

    部署节点

    10.0.1.121

    作为宿主机通过kubeasz容器运行ansible/ezctl命令

    etcd节点

    10.0.1.122

    10.0.1.123

    10.0.1.124

    注意etcd集群需要1,3,5,...奇数个节点,本实战安装3个节点

    master节点

    10.0.1.122

    10.0.1.123

    10.0.1.124

    高可用集群至少2个master节点,本实战安装3个节点

    node节点

    10.0.1.125

    10.0.1.126

    10.0.1.127

    运行应用负载的节点,节点数任意,本实战安装3个节点

    以上服务器均是在一台普通台式机开的虚拟机,配置均是1c4g40g,最小化安装centos7.9。本次部署将以10.0.1.121作为宿主机通过kubeasz容器在线安装k8s集群,其中kubeasz使用即时的最新版本3.3.0。部署的k8s集群版本信息如下:

    •k8s: v1.24.1

    •etcd: v3.5.4

    •containerd: 1.6.4

    •flanal: v0.15.1

    •dashboard: v2.5.1

    安装部署

    以下所有操作在部署节点10.0.1.121完成。

    准备环境
    准备脚本、二进制文件和镜像文件
    1. # 下载工具脚本ezdown,使用kubeasz版本3.3.0
    2. export release=3.3.0
    3. wget https://github.com/easzlab/kubeasz/releases/download/${release}/ezdown
    4. chmod +x ./ezdown
    5. # 使用工具脚本下载
    6. ./ezdown -D

    ./ezdown -D命令多执行几遍,直至再执行的时候不再downloading为止。这样就在/etc/kubeasz目录下下载了在线安装所有需要的脚本、二进制文件和镜像文件。

    另外,通过执行./ezdown -D我们会发现docker环境也在主控宿主机安装好了,这个效果即使在做非k8集群本地化交付场景的时候也特别有用。

    1. [root@server121 ~]# docker version
    2. Client:
    3. Version: 20.10.16
    4. API version: 1.41
    5. Go version: go1.17.10
    6. Git commit: aa7e414
    7. Built: Thu May 12 09:14:28 2022
    8. OS/Arch: linux/amd64
    9. Context: default
    10. Experimental: true
    11. Server: Docker Engine - Community
    12. Engine:
    13. Version: 20.10.16
    14. API version: 1.41 (minimum version 1.12)
    15. Go version: go1.17.10
    16. Git commit: f756502
    17. Built: Thu May 12 09:19:16 2022
    18. OS/Arch: linux/amd64
    19. Experimental: false
    20. containerd:
    21. Version: v1.6.4
    22. GitCommit: 212e8b6fa2f44b9c21b2798135fc6fb7c53efc16
    23. runc:
    24. Version: 1.1.1
    25. GitCommit: v1.1.1-0-g52de29d7
    26. docker-init:
    27. Version: 0.19.0
    28.   GitCommit:        de40ad0

    如果目标服务器可以直接连外网,那么通过./ezdown -D下载的文件足够用了,但是如果目标服务器不能连外网,那么我们还需要通过另外一个命令./ezdown -P把离线文件也下载下来,这些文件也都在/etc/kubeasz。后面我们把/etc/kubeasz这个目录打个压缩包,并和ezdown文件归档在一起后续可以直接使用。

    设置ssh免密登陆部署服务器
    1. # 主控机设置密钥
    2. ssh-keygen
    3. # 设置免密登陆
    4. ssh-copy-id 10.0.1.122
    5. ssh-copy-id 10.0.1.123
    6. ssh-copy-id 10.0.1.124
    7. ssh-copy-id 10.0.1.125
    8. ssh-copy-id 10.0.1.126
    9. ssh-copy-id 10.0.1.127
    启动kubeasz容器
    1. ./ezdown -S
    2. 2022-07-01 19:56:27 INFO Action begin: start_kubeasz_docker
    3. 2022-07-01 19:56:27 INFO try to run kubeasz in a container
    4. 2022-07-01 19:56:27 DEBUG get host IP: 10.0.1.121
    5. b2d5eeeaba3a: Loading layer [==================================================>] 5.88MB/5.88MB
    6. ff362123de14: Loading layer [==================================================>] 2.852MB/2.852MB
    7. d90070ff2c3a: Loading layer [==================================================>] 30.32MB/30.32MB
    8. b1ed2c7094f4: Loading layer [==================================================>] 4.608kB/4.608kB
    9. 9f48806e63e9: Loading layer [==================================================>] 8.55MB/8.55MB
    10. ab2737de92cb: Loading layer [==================================================>] 128.6MB/128.6MB
    11. 7a12142df705: Loading layer [==================================================>] 2.838MB/2.838MB
    12. Loaded image: easzlab/kubeasz:3.3.0
    13. d037e087572e16c07c7f5d3469decbc4cedc4aaca79dffd2d73ab2a4fa69f3c7
    14. 2022-06-30 20:34:24 INFO Action successed: start_kubeasz_docker
    进入kubeasz创建集群k8s-01
    1. docker exec -it kubeasz bash
    2. bash-5.1# ezctl new k8s-01
    3. 2022-07-01 19:58:27 DEBUG generate custom cluster files in /etc/kubeasz/clusters/k8s-01
    4. 2022-07-01 19:58:27 DEBUG set version of common plugins
    5. 2022-07-01 19:58:27 DEBUG cluster k8s-01: files successfully created.
    6. 2021-01-19 10:48:23 INFO next steps 1: to config '/etc/kubeasz/clusters/k8s-01/hosts'
    7. 2021-01-19 10:48:23 INFO next steps 2to config '/etc/kubeasz/clusters/k8s-01/config.yml'

    根据提示修改hosts如下,config.yml保持不变。其中hosts文件中按规划调整了etcd、kube_master、kube_node和ex_lb四处位置的服务器IP,注意这里只能使用IP,不能使用hostname;另外CONTAINER_RUNTIME应该设置为containerd,CLUSTER_NETWORK设置为flannel,其它配置可保持不变。

    注意:

    •生成的hosts文件里的CONTAINER_RUNTIME一定不能是docker,这个不要调整错了。

    •CLUSTER_NETWORK默认是calico,但我按这个设置安装会遇到问题,所以切换成flannel了。

    1. # 'etcd' cluster should have odd member(s) (1,3,5,...)
    2. [etcd]
    3. 10.0.1.122
    4. 10.0.1.123
    5. 10.0.1.124
    6. # master node(s)
    7. [kube_master]
    8. 10.0.1.122
    9. 10.0.1.123
    10. 10.0.1.124
    11. # work node(s)
    12. [kube_node]
    13. 10.0.1.125
    14. 10.0.1.126
    15. 10.0.1.127
    16. # [optional] harbor server, a private docker registry
    17. # 'NEW_INSTALL': 'true' to install a harbor server; 'false' to integrate with existed one
    18. [harbor]
    19. #192.168.1.8 NEW_INSTALL=false
    20. # [optional] loadbalance for accessing k8s from outside
    21. [ex_lb]
    22. 10.0.1.125 LB_ROLE=backup EX_APISERVER_VIP=10.0.1.200 EX_APISERVER_PORT=8443
    23. 10.0.1.126 LB_ROLE=master EX_APISERVER_VIP=10.0.1.200 EX_APISERVER_PORT=8443
    24. # [optional] ntp server for the cluster
    25. [chrony]
    26. #192.168.1.1
    27. [all:vars]
    28. # --------- Main Variables ---------------
    29. # Secure port for apiservers
    30. SECURE_PORT="6443"
    31. # Cluster container-runtime supported: docker, containerd
    32. # if k8s version >= 1.24, docker is not supported
    33. CONTAINER_RUNTIME="containerd"
    34. # Network plugins supported: calico, flannel, kube-router, cilium, kube-ovn
    35. CLUSTER_NETWORK="flannel"
    36. # Service proxy mode of kube-proxy: 'iptables' or 'ipvs'
    37. PROXY_MODE="ipvs"
    38. # K8S Service CIDR, not overlap with node(host) networking
    39. SERVICE_CIDR="10.68.0.0/16"
    40. # Cluster CIDR (Pod CIDR), not overlap with node(host) networking
    41. CLUSTER_CIDR="172.20.0.0/16"
    42. # NodePort Range
    43. NODE_PORT_RANGE="30000-32767"
    44. # Cluster DNS Domain
    45. CLUSTER_DNS_DOMAIN="cluster.local"
    46. # -------- Additional Variables (don't change the default value right now) ---
    47. # Binaries Directory
    48. bin_dir="/opt/kube/bin"
    49. # Deploy Directory (kubeasz workspace)
    50. base_dir="/etc/kubeasz"
    51. # Directory for a specific cluster
    52. cluster_dir="{{ base_dir }}/clusters/k8s-01"
    53. # CA and other components cert/key Directory
    54. ca_dir="/etc/kubernetes/ssl"

    默认安装模式是在线安装,这里我们调整为离线安装模式

    sed -i 's/^INSTALL_SOURCE.*$/INSTALL_SOURCE: "offline"/g' /etc/kubeasz/clusters/k8s-01/config.yml
    开始安装
    1. docker exec -it kubeasz bash
    2. # 一键安装,等价于执行docker exec -it kubeasz ezctl setup k8s-01 all
    3. bash-5.1# ezctl setup k8s-01 all
    4. # 分步安装 需要分别执行01-07的yml
    5. # 01-创建证书和环境准备
    6. bash-5.1# ezctl setup k8s-01 01
    7. # 02-安装etcd集群
    8. bash-5.1# ezctl setup k8s-01 02
    9. # 03-安装容器运行时(docker or containerd)
    10. bash-5.1# ezctl setup k8s-01 03
    11. # 04-安装kube_master节点
    12. bash-5.1# ezctl setup k8s-01 04
    13. # 05-安装kube_node节点
    14. bash-5.1# ezctl setup k8s-01 05
    15. # 06-安装网络组件
    16. bash-5.1# ezctl setup k8s-01 06
    17. # 07-安装集群主要插件
    18. bash-5.1# ezctl setup k8s-01 07
    19. 执行一键安装,静候安装完成,显示如下就是安装好了。也可以分步安装。按照kubeasz对安装kubernetes集群的步骤拆解,一共分为7步,每一步都对应相应的安装任务,读者可以自行体验。
    20. PLAY RECAP *******************************************************************************************************************************************************************************************************************
    21. 10.0.1.122 : ok=117 changed=85 unreachable=0 failed=0 skipped=187 rescued=0 ignored=0
    22. 10.0.1.123 : ok=112 changed=81 unreachable=0 failed=0 skipped=169 rescued=0 ignored=0
    23. 10.0.1.124 : ok=112 changed=81 unreachable=0 failed=0 skipped=169 rescued=0 ignored=0
    24. 10.0.1.125 : ok=107 changed=74 unreachable=0 failed=0 skipped=192 rescued=0 ignored=0
    25. 10.0.1.126 : ok=96 changed=68 unreachable=0 failed=0 skipped=177 rescued=0 ignored=0
    26. 10.0.1.127 : ok=96 changed=68 unreachable=0 failed=0 skipped=177 rescued=0 ignored=0
    27. localhost : ok=32 changed=22 unreachable=0 failed=0 skipped=15 rescued=0 ignored=0
    28. 验证安装
    29. 登陆任意一台Master,我们确认下是否安装成功。
    30. # 可以看到各节点就绪 (Ready) 状态、角色、运行时间以及版本号
    31. [root@server122 ~]# kubectl get nodes
    32. NAME STATUS ROLES AGE VERSION
    33. 10.0.1.122 Ready,SchedulingDisabled master 10h v1.24.1
    34. 10.0.1.123 Ready,SchedulingDisabled master 10h v1.24.1
    35. 10.0.1.124 Ready,SchedulingDisabled master 10h v1.24.1
    36. 10.0.1.125 Ready node 10h v1.24.1
    37. 10.0.1.126 Ready node 10h v1.24.1
    38. 10.0.1.127 Ready node 10h v1.24.1
    39. # 可以看到scheduler/controller-manager/etcd等组件 Healthy
    40. [root@server122 ~]# kubectl get cs
    41. Warning: v1 ComponentStatus is deprecated in v1.19+
    42. NAME STATUS MESSAGE ERROR
    43. scheduler Healthy ok
    44. etcd-1 Healthy {"health":"true","reason":""}
    45. etcd-2 Healthy {"health":"true","reason":""}
    46. etcd-0 Healthy {"health":"true","reason":""}
    47. controller-manager Healthy ok
    48. # 可以看到kubernetes master(apiserver)组件 running
    49. [root@server122 ~]# kubectl cluster-info
    50. Kubernetes control plane is running at https://127.0.0.1:6443
    51. CoreDNS is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    52. KubeDNSUpstream is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/kube-dns-upstream:dns/proxy
    53. kubernetes-dashboard is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy
    54. # 可以查看所有集群pod状态,默认已安装网络插件flannel、coredns、metrics-server等
    55. [root@server122 ~]# kubectl get po --all-namespaces
    56. NAMESPACE NAME READY STATUS RESTARTS AGE
    57. kube-system coredns-ff4774677-xm6dq 1/1 Running 0 13h
    58. kube-system dashboard-metrics-scraper-8c47d4b5d-lpzcd 1/1 Running 0 13h
    59. kube-system kube-flannel-ds-gnwvt 1/1 Running 0 13h
    60. kube-system kube-flannel-ds-jh87x 1/1 Running 0 13h
    61. kube-system kube-flannel-ds-lrlcm 1/1 Running 0 13h
    62. kube-system kube-flannel-ds-m7xqp 1/1 Running 0 13h
    63. kube-system kube-flannel-ds-nj82h 1/1 Running 0 13h
    64. kube-system kube-flannel-ds-xm6jc 1/1 Running 0 13h
    65. kube-system kubernetes-dashboard-5d46f4c997-44d4r 1/1 Running 0 13h
    66. kube-system metrics-server-56646b5b79-k9sdr 1/1 Running 0 13h
    67. kube-system node-local-dns-6dh4c 1/1 Running 0 13h
    68. kube-system node-local-dns-dtktz 1/1 Running 0 13h
    69. kube-system node-local-dns-ln9kk 1/1 Running 0 13h
    70. kube-system node-local-dns-mvmvf 1/1 Running 0 13h
    71. kube-system node-local-dns-nl7bq 1/1 Running 0 13h
    72. kube-system node-local-dns-tnwpj 1/1 Running 0 13h
    73. # 可以查看所有集群svc状态
    74. [root@server122 ~]# kubectl get svc --all-namespaces
    75. kubectl get svc --all-namespaces
    76. NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    77. default kubernetes ClusterIP 10.68.0.1 443/TCP 14h
    78. kube-system dashboard-metrics-scraper ClusterIP 10.68.44.158 8000/TCP 14h
    79. kube-system kube-dns ClusterIP 10.68.0.2 53/UDP,53/TCP,9153/TCP 14h
    80. kube-system kube-dns-upstream ClusterIP 10.68.126.39 53/UDP,53/TCP 14h
    81. kube-system kubernetes-dashboard NodePort 10.68.227.190 443:32360/TCP 14h
    82. kube-system metrics-server ClusterIP 10.68.25.222 443/TCP 14h
    83. kube-system   node-local-dns              ClusterIP   None                    9253/TCP

    至此,我们的k8s集群安装验证成功,一套6节点的集群,从开始安装到完成大概需要10分钟。这样的效率确实是让人很满意的(笔者运行这7台虚拟机的台式机网卡网速是1000M)。

    访问kubernetes-dashboard

    在验证安装过程查看所有集群svc状态的操作中,我们看到了默认安装了kubernetes-dashboard,我们可以通过NodePort访问https://${IP}:32360/,其中${IP}可为任一节点IP。

    这里我们使用chrome浏览器访问,显示如下

    908f6425d66f1410c31941733bc595f4.png

    随便点击页面的空白处,然后输入:thisisunsafe
    页面正常打开如下

    870663143c3616ae463b28c35a118670.png

    从master节点查看token并使用token登陆(这里为了方便,我们可以直接使用admin-user的token)

    1. # 查看内容含有token的secret
    2. [root@server122 ~]# kubectl get secret -n kube-system
    3. NAME TYPE DATA AGE
    4. admin-user kubernetes.io/service-account-token 3 15h
    5. dashboard-read-user kubernetes.io/service-account-token 3 15h
    6. kubernetes-dashboard-certs Opaque 0 15h
    7. kubernetes-dashboard-csrf Opaque 1 15h
    8. kubernetes-dashboard-key-holder Opaque 2 15h
    9. # 查看admin-user对应的token
    10. [root@server122 ~]# kubectl describe secret -n kube-system admin-user
    11. Name: admin-user
    12. Namespace: kube-system
    13. Labels:
    14. Annotations: kubernetes.io/service-account.name: admin-user
    15. kubernetes.io/service-account.uid: 1d698eb4-e299-44d7-9c04-84b8981a64df
    16. Type: kubernetes.io/service-account-token
    17. Data
    18. ====
    19. ca.crt: 1302 bytes
    20. namespace: 11 bytes
    21. token: eyJhbGciOiJSUzI1NiIsImtpZCI6IlFLdmIxYUNmclEzenhfczFWdkJySU04VF9XYm9tZzExVTlFbHBNRDJRUTgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIxZDY5OGViNC1lMjk5LTQ0ZDctOWMwNC04NGI4OTgxYTY0ZGYiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4tdXNlciJ9.a6qxey5eRrRPi7T5RSFy-mZMaAu1YsMIrBVZdbj8g0-kNC06fYLOtFIezPtp3drHSUzOz31TTSZ_0PoU4PYAS9-c_IabiMaf4qhN0BJ05JpX17SDZ5PlbD-7jh2bqpkd96tr3BDZQpb_NYk_vwiCYCItMEoHNvihjopp4lNG0cxxZEvyZ1X8wKDIQq401lO2xrfP13Y3eE3FkZYq9eQEw0RY9YOyDemMikvVWKTdTRjy9RMOG4rxRYROZMJHU_KtmOjBZp21NDVyCYnxeQfw9hLxj0roJAWEsDe_NCE4_WFm5JcAeH5tC1YaRuDhotXaeqN46lDPzB-KDBW8dB1EEQ

    使用admin-user的token登陆kubernetes-dashboard,进入后显示如下

    6c5c49454f5a8a399a6ee948ac849a54.png


    扩展使用

    kubeasz除了用来运维x86架构kubernetes集群外,还可以在不限于如下场景发挥积极作用。

    docker离线交付(本地交付)

    kubeasz的分步安装中,第一步是创建证书和环境准备,其中环境准备是对所有目标服务器进行一些基础设置,能够调整操作系统的性能基线,这个也是适用于非容器化环境的服务器的;第三步是安装容器运行时,可以选择docker或者containerd,只需要调整如/etc/kubeasz/clusters/${CLUSTER_NAME}/hosts文件中的CONTAINER_RUNTIME为docker就可以后,再执行第三步,我们就可以在所有的目标服务器统一安装docker。

    ARM架构适配改造

    当下的kubeasz其实是只支持x86服务器架构的,并不能直接支持arm架构服务器。但笔者相信通过一定的改造,其安装包也是可以适配arm架构服务器的。笔者已经做过如下的简单尝试,有真实需求的读者可以继续深入。

    笔者在kubeasz的在线安装包上了做了一些改动,比如置换cfssl、cfssl-certinfo和cfssljson为适配arm架构的二进制;比如下载了kubectl的arm版本二进制命名kubectl-arm64后放在/etc/kubeadz/bin目录下,同时修改了/etc/kubeadz/roles若干的roles定义等等后使用自行构建的适配arm架构的镜像pi4k8s/easzlab-kubasz:3.3.0在树莓派PI4B(raspios-arm64)上成功远程部署了一套x86架构的k8s集群。

    总结

    本次实战表明,我们使用kubeasz可以在很短的时间内部署一套二进制部署的次新高版本x86架构多master高可用k8s集群,在这个基础上,我们只需要集成helm、ingress、prometheus、storage等组件后,这套集群就可以迅速交付使用。

    众所周知,在k8s集群应用和实践过程中,一般业务开发工程师除了会使用一些简单容器化命令外,对于k8s集群本身或者关联的问题,往往会觉得很棘手,需要协调k8s运维工程师深度协作才能分析、定位和解决,究其原因,就是k8s集群的搭建和运维涉及组件众多,关系复杂,业务开发工程师在遇到问题的时候,总难免畏手畏脚,非常担心操作失当,导致更复杂问题的出现,典型的就是把k8s集群弄得不正常,又非常不好协调k8s运维工程师处理,项目进展因此雪上加霜。因为在大多数公司,业务开发工程师和运维工程师分属不同的团队或者部门,协调起来并不容易;另外其实还有些公司可能连做k8s运维的工程师都没有,更谈不上协调了,这时候就只能靠业务开发工程师自行探索解决了。kubeasz这样的神器,除了能提高k8s集群交付效率外,也正因为大幅度降低了k8s集群构建成本,从而也相应减少了业务开发工程师面对k8s集群的畏难情绪,提升了学习和使用热情,对其培养k8s环境的熟悉和敏感程度非常有价值,而这一点其实非常重要。

    另外,kubeasz除了用来运维x86架构kubernetes集群外,也可以扩展到其他场景,比如做docker的离线交付,比如做ARM架构适配改造等。

    作者介绍:

    崔莹峰,一名70后程序员,拥有10多年工作经验,长期从事 Java 开发,架构设计,容器化等相关工作。精通Java,熟练使用Maven、Jenkins等Devops相关工具链,擅长容器化方案规划、设计和落地。

      往期推荐:

    d233b4277cf0aa3837eb685fd082f39d.jpeg

    长按二维码关注

    以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。

  • 相关阅读:
    鸿蒙Next怎么升级,有便捷的方法?
    文本检测及识别小组周报
    02_Docker
    【学习笔记】深度学习入门:基于Python的理论与实现-误差反向传播法
    【C++】构造函数分类 ① ( 构造函数分类简介 | 无参构造函数 | 有参构造函数 | 拷贝构造函数 | 代码示例 - 三种类型构造函数定义与调用 )
    BluePrism注册下载并安装-RPA第一章
    Flask笔记一之项目搭建、配置项导入
    Invest模型问题答疑--产水模块、土壤保持模块、供需平衡分析、生态系统服务、生物多样性生境质量
    关于自定义程序打包成jar包,并读取配置
    NFTScan 正式上线 TON NFTScan 浏览器!
  • 原文地址:https://blog.csdn.net/u013527895/article/details/126672642