• k8s-16_Kubernetes-日常维护总结


    文章目录

    k8s-16_Kubernetes-日常维护总结

    1.更改证书有效期

    一、基础知识

    1. k8s使用https双向认证,使用kubeadm安装后证书默认有效期为1年

    2. k8s证书存放路径:/etc/kubernetes/pki/

    3. 查看证书信息:openssl x509 -in apiserver.crt -text -noout

      修改方式:修改kubeadm源码,更改证书默认有效期

      image-20220816171744672

    image-20220816171643630

    二、go 环境部署

    下载go安装包
    wget https://studygolang.com/dl/golang/go1.13.7.linux-amd64.tar.gz
    2. 解压到/usr/local下
    tar -zxvf go1.13.7.linux-amd64.tar.gz -C /usr/local/
    3. 修改环境变量
    vim /etc/profile
    export PATH=$PATH:/usr/local/go/bin
    4. 加载环境变量配置
    source /etc/profile
    5. 查看版本信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20220816171903649

    image-20220816172028953

    三、git源码

    下载源码至本地
    git clone https://github.com/kubernetes/kubernetes.git
    
    • 1
    查看当前k8s版本
    kubeadm version
    
    • 1
    切换git到指定版本
    cd kubernetes
    git checkout -b remotes/origin/release-1.17.0 v1.17.0
    
    • 1
    • 2

    image-20220817134139497

    四、修改源码更新证书策略

    查看相关代码所在文件名称
    grep -r kubeadmconstants.CertificateValidity .
    
    • 1

    image-20220817135830973

    更改源代码
    root@k8s-master:~/git/kubernetes# cd cmd/kubeadm/app/util/pkiutil/
    root@k8s-master:~/git/kubernetes/cmd/kubeadm/app/util/pkiutil# ls
    pki_helpers.go	pki_helpers_test.go  testing
    root@k8s-master:~/git/kubernetes/cmd/kubeadm/app/util/pkiutil# vim pki_helpers.go
    
    • 1
    • 2
    • 3
    • 4

    修改如下
    搜索 serial字段 添加如下

    637         const duration365d = time.Hour * 24 * 365
    
    • 1

    搜索 DNSNames 添加如下

    667                 NotAfter:              time.Now()Add(duration365d * 10).UTC(),
    
    • 1

    最新版本
    image-20220817140956722

    老版本image-20220817134905411

    编译代码kubeadm
    root@k8s-master:~/git/kubernetes# pwd
    /root/git/kubernetes
    root@k8s-master:~/git/kubernetes# make WHAT=cmd/kubeadm GOFLAGS=-v
    
    • 1
    • 2
    • 3
    升级go到go1.17.0版本

    这里go 版本不够,我们进行一下升级,我们安装go1.17.0

    root@k8s-master:~/git/kubernetes# cd /usr/local/
    root@k8s-master:/usr/local# ls
    bin  etc  games  go  include  lib  man	sbin  share  src
    root@k8s-master:/usr/local# mv go go.1.13.7.bak
    root@k8s-master:/usr/local# go version
    -bash: /usr/local/go/bin/go: 没有那个文件或目录   做备份即可
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    image-20220817141152212

    安装下载最新的go

    wget https://go.dev/dl/go1.17.6.linux-amd64.tar.gz
    root@k8s-master:~/go# tar -zxf go1.17.6.linux-amd64.tar.gz -C /usr/local/
    root@k8s-master:~/go# source /etc/profile
    root@k8s-master:~/go# go version
    go version go1.17.6 linux/amd64
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220817142558259

    接着编译

    root@k8s-master:~/git/kubernetes# make WHAT=cmd/kubeadm GOFLAGS=-v
    
    chmod +x _output/bin/prerelease-lifecycle-gen
    
    • 1
    • 2
    • 3
    将编译成功的kubeadm复制到/root下
    kubernetes] #cp _output/bin/kubeadm /root/kubeadm-new
    
    • 1

    五、更新 kubeadm

    备份原kubeadm文件
    cp /usr/bin/kubeadm /usr/bin/kubeadm.old
    
    • 1
    替换新kubeadm文件
    cp /root/kubeadm-new /usr/bin/kubeadm
    
    • 1
    赋予执行权限
    chmod a+x /usr/bin/kubeadm
    
    • 1

    六、更新master节点证书

    备份原证书文件
    cp -r /etc/kubernetes/pki /etc/kubernetes/pki.old
    
    • 1
    生成新证书
    cd /etc/kubernetes/pki
    kubeadm alpha certs renew all --config kubeadm-config.yaml
    
    • 1
    • 2
    查看证书有效期
    openssl x509 -in apiserver.crt -text -noout | grep Not
    
    • 1

    image-20220817145417722

    重新初始化集群
    kubeadm init phase kubeconfig all --config kubeadm-config.yaml
    
    • 1
    重新生成kubeconfig文件
    mv $HOME/.kube/config $HOME/.kube/config.old
    cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
    chown $(id -u):$(id -g) $HOME/.kube/config
    
    • 1
    • 2
    • 3
    更新docker容器证书
    # 查看容器证书有效期
    echo | openssl s_client -showcerts -connect 127.0.0.1:6443 -servername api 2>/dev/null | openssl x509 -noout -enddate
    
    docker restart `docker ps | grep etcd | awk '{ print $1 }'`
    docker restart `docker ps | grep kube-apiserver | awk '{ print $1 }'`
    docker restart `docker ps | grep kube-scheduler | awk '{ print $1 }'`
    docker restart `docker ps | grep kube-controller | awk '{ print $1 }'`
    
    # 查看更新后的证书有限期
    echo | openssl s_client -showcerts -connect 127.0.0.1:6443 -servername api 2>/dev/null | openssl x509 -noout -enddate
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    更新kubelet
    vim /etc/kubernetes/manifests/kube-controller-manager.yaml
    spec:
      containers:
      - command:
        - kube-controller-manager
        # 新增以下两行参数
        - --experimental-cluster-signing-duration=87600h0m0s
        - --feature-gates=RotateKubeletServerCertificate=true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    七、HA其余master节点证书更新

    #!/bin/bash
    masterNode="192.168.66.20 192.168.66.21"
    #for host in ${masterNode}; do
    #    scp /etc/kubernetes/pki/{ca.crt,ca.key,sa.key,sa.pub,front-proxy-ca.crt,front-proxy-ca.key}"
    ${USER}"@$host:/etc/kubernetes/pki/
    #    scp /etc/kubernetes/pki/etcd/{ca.crt,ca.key} "root"@$host:/etc/kubernetes/pki/etcd
    #    scp /etc/kubernetes/admin.conf "root"@$host:/etc/kubernetes/
    #done
    for host in${CONTROL_PLANE_IPS}; do
        scp /etc/kubernetes/pki/{ca.crt,ca.key,sa.key,sa.pub,front-proxy-ca.crt,front-proxy-ca.key}"${USER}"@$host:/root/pki/
        scp /etc/kubernetes/pki/etcd/{ca.crt,ca.key} "root"@$host:/root/etcd   
        scp /etc/kubernetes/admin.conf "root"@$host:/root/kubernetes/
    done
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    八、其他方法更改证书

    kubeadm 默认证书为一年,一年过期后,会导致api service不可用,使用过程中会出现:x509: certificate has expired or is not yet valid.

    Google 建议通过不停更新版本来自动更新证书,太坑_

    可以在初始化群集之前重新编译kubeadm,证书有效期自动为100年

    获取源码

    访问:https://github.com/kubernetes/kubernetes/releases,下载特定版本源码

    wget https://github.com/kubernetes/kubernetes/archive/v1.23.0.tar.gz
    tar -zxvf kubernetes-1.23.0.tar.gz
    mv kubernetes-1.23.0 kubernetes
    cd kubernetes
    
    • 1
    • 2
    • 3
    • 4

    或者使用 git 获取

    # yum install git
    git clone https://github.com/kubernetes/kubernetes.git
    cd kubernetes
    git checkout -b remotes/origin/release-1.23 v1.23.0
    
    • 1
    • 2
    • 3
    • 4
    修改证书有效期

    查看网上的资料主要有两个地方需要修改

    修改 CA 有效期为 100 年(默认为 10 年)
    vim ./staging/src/k8s.io/client-go/util/cert/cert.go
    
    • 1
    // 这个方法里面 NotAfter:              now.Add(duration365d * 10).UTC()
    // 默认有效期就是 10 年,改成 100(sysin)
    // 输入 /NotAfter 查找,回车定位
    func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) {
            now := time.Now()
            tmpl := x509.Certificate{
                    SerialNumber: new(big.Int).SetInt64(0),
                    Subject: pkix.Name{
                            CommonName:   cfg.CommonName,
                            Organization: cfg.Organization,
                    },
                    NotBefore:             now.UTC(),
                    // NotAfter:              now.Add(duration365d * 10).UTC(),
                    NotAfter:              now.Add(duration365d * 100).UTC(),
                    KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
                    BasicConstraintsValid: true,
                    IsCA:                  true,
            }
    
            certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
            if err != nil {
                    return nil, err
            }
            return x509.ParseCertificate(certDERBytes)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    修改证书有效期为 100 年(默认为 1 年)
    vim ./cmd/kubeadm/app/constants/constants.go
    
    • 1
    // 就是这个常量定义CertificateValidity,改成*100年
    const (
            // KubernetesDir is the directory Kubernetes owns for storing various configuration files
            KubernetesDir = "/etc/kubernetes"
            // ManifestsSubDirName defines directory name to store manifests
            ManifestsSubDirName = "manifests"
            // TempDirForKubeadm defines temporary directory for kubeadm
            // should be joined with KubernetesDir.
            TempDirForKubeadm = "tmp"
    
            // CertificateValidity defines the validity for all the signed certificates generated by kubeadm
            // CertificateValidity = time.Hour * 24 * 365
            CertificateValidity = time.Hour * 24 * 365 * 100
    
            // CACertAndKeyBaseName defines certificate authority base name
            CACertAndKeyBaseName = "ca"
            // CACertName defines certificate name
            CACertName = "ca.crt"
            // CAKeyName defines certificate name
            CAKeyName = "ca.key"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    git验证,修改的内容如下:
    git diff
    
    diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go
    index 75adf43..54f25fa 100644
    --- a/cmd/kubeadm/app/constants/constants.go
    +++ b/cmd/kubeadm/app/constants/constants.go
    @@ -44,7 +44,7 @@ const (
            TempDirForKubeadm = "tmp"
    
            // CertificateValidity defines the validity for all the signed certificates generated by kubeadm
    -       CertificateValidity = time.Hour * 24 * 365
    +       CertificateValidity = time.Hour * 24 * 365 * 100
    
            // CACertAndKeyBaseName defines certificate authority base name
            CACertAndKeyBaseName = "ca"
    diff --git a/staging/src/k8s.io/client-go/util/cert/cert.go b/staging/src/k8s.io/client-go/util/cert/cert.go
    index 9fd097a..865d6bb 100644
    --- a/staging/src/k8s.io/client-go/util/cert/cert.go
    +++ b/staging/src/k8s.io/client-go/util/cert/cert.go
    @@ -63,7 +63,7 @@ func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, erro
                            Organization: cfg.Organization,
                    },
                    NotBefore:             now.UTC(),
    -               NotAfter:              now.Add(duration365d * 10).UTC(),
    +               NotAfter:              now.Add(duration365d * 100).UTC(),
                    KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
                    BasicConstraintsValid: true,
                    IsCA:                  true,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    源代码改好了,接下来就是编译kubeadm了

    Docker镜像编译(推荐)
    • 查看 kube-cross 的 TAG 版本号
    # cat ./build/build-image/cross/VERSION
    
    v1.13.8-1
    
    • 1
    • 2
    • 3

    这里我们可以使用官方容器对代码进行编译:k8s.gcr.io/kube-cross:v1.13.6-1(当前只有1.13.6而不是1.13.8,未知)

    • 拉取镜像

    无法翻墙可以用下面的替代镜像:

    docker pull gcrcontainer/kube-cross:v1.13.6-1
    
    或者:
    docker pull registry.aliyuncs.com/google_containers/kube-cross:v1.13.6-1
    
    • 1
    • 2
    • 3
    • 4

    image-20220817151032187

    • 编译
    # docker run --rm -v <你修改后的代码目录>:/go/src/k8s.io/kubernetes -it gcrcontainer/kube-cross bash
    docker run --rm -v /root/kubernetes:/go/src/k8s.io/kubernetes -it gcrcontainer/kube-cross:v1.13.6-1 bash
    
    cd /go/src/k8s.io/kubernetes
    
    # 编译kubeadm, 这里主要编译kubeadm 即可
    make all WHAT=cmd/kubeadm GOFLAGS=-v
    
    # 编译kubelet
    # make all WHAT=cmd/kubelet GOFLAGS=-v
    
    # 编译kubectl
    # make all WHAT=cmd/kubectl GOFLAGS=-v
    
    # 退出容器
    exit
    
    #编译完产物在 _output/bin/kubeadm 目录下,
    #其中bin是使用了软连接
    #真实路径是_output/local/bin/linux/amd64/kubeadm
    mv /usr/bin/kubeadm /usr/bin/kubeadm_backup
    cp _output/local/bin/linux/amd64/kubeadm /usr/bin/kubeadm
    #chmod +x /usr/bin/kubeadm
    
    # 验证版本
    kubeadm version
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    软件包准备

    CentOS:

    yum install gcc make -y
    yum install rsync jq -y
    
    • 1
    • 2

    Ubuntu:

    sudo apt install build-essential #(Following command will install essential commands like gcc, make etc.)
    sudo apt install rsync jq -y
    
    • 1
    • 2
    GoLang 环境

    查看 kube-cross 的 TAG 版本号

    # cat ./build/build-image/cross/VERSION
    
    v1.13.8-1
    
    • 1
    • 2
    • 3
    • 安装Go环境:
    wget https://dl.google.com/go/go1.13.8.linux-amd64.tar.gz
    tar zxvf go1.13.8.linux-amd64.tar.gz  -C /usr/local
    
    # 编辑/etc/profile文件添加如下:
    #go setting
    export GOROOT=/usr/local/go
    export GOPATH=/usr/local/gopath
    export PATH=$PATH:$GOROOT/bin
    
    #生效
    source /etc/profile
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 验证:
    go version
    go version go1.13.8 linux/amd64
    
    • 1
    • 2
    • 编译:
    # 编译kubeadm, 这里主要编译kubeadm 即可
    make all WHAT=cmd/kubeadm GOFLAGS=-v
    
    # 编译kubelet
    # make all WHAT=cmd/kubelet GOFLAGS=-v
    
    # 编译kubectl
    # make all WHAT=cmd/kubectl GOFLAGS=-v
    
    #编译完产物在 _output/bin/kubeadm 目录下,
    #其中bin是使用了软连接
    #真实路径是_output/local/bin/linux/amd64/kubeadm
    mv /usr/bin/kubeadm /usr/bin/kubeadm_backup
    cp _output/local/bin/linux/amd64/kubeadm /usr/bin/kubeadm
    chmod +x /usr/bin/kubeadm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220817155657629

    执行命令更新证书

    可以先备份证书,证书在/etc/kubernetes/pki

    cp -r /etc/kubernetes/pki /etc/kubernetes/pki.old
    
    • 1
    • 检查证书到期时间
    新版本(1.15+):kubeadm alpha certs check-expiration
    或
    openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text |grep ' Not '
    其他同理
    
    • 1
    • 2
    • 3
    • 4
    证书备份
    cp -rp /etc/kubernetes /etc/kubernetes.bak
    
    
    • 1
    • 2
    移除过期证书
    rm -f /etc/kubernetes/pki/apiserver*
    rm -f /etc/kubernetes/pki/front-proxy-client.*
    rm -rf /etc/kubernetes/pki/etcd/healthcheck-client.*
    rm -rf /etc/kubernetes/pki/etcd/server.*
    rm -rf /etc/kubernetes/pki/etcd/peer.*
    
    • 1
    • 2
    • 3
    • 4
    • 5

    备注:可以使用命令openssl x509 -in [证书全路径] -noout -text查看证书详情。

    重新生成证书
    老版本:kubeadm alpha phase certs all
    或
    新版本(1.15+):kubeadm alpha certs renew all 使用该命令不用提前删除过期证书
    
    
    • 1
    • 2
    • 3
    • 4
    重新生成配置文件
    # 重新生成配置
    mv /etc/kubernetes/*.conf /tmp/
    老版本:kubeadm alpha phase kubeconfig all
    或
    新版本(1.15+):kubeadm init phase kubeconfig all
    # 更新kubectl配置
    cp /etc/kubernetes/admin.conf ~/.kube/config
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    重启kubelet
    systemctl restart kubelet
    
    • 1
    证书过期时间确认
    openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text |grep ' Not '
    其他同理
    
    • 1
    • 2

    2.k8s版本升级

    一、准备工作

    在master节点上查看此时的kubernetes的版本

    image-20220817162639059

    查询最新版本号
    yum list kubeadm --showduplicates | sort -r
    
    • 1

    二、升级操作

    master和node节点执行升级命令
    yum update -y kubeadm kubectl kubelet
    
    • 1
    master节点验证升级版本
    kubeadm upgrade plan
    
    • 1

    image-20220817162832044

    升级到指定版本
    kubeadm upgrade apply v1.17.2
    
    • 1

    image-20220817162856859

    master和node重启kubelet
    systemctl daemon-reload
    systemctl restart kubelet
    
    • 1
    • 2

    三、验证

    查看node信息

    image-20220817163001347

    3.添加work节点

    work节点进行初始化操作
    master节点查询join命令
    kubeadm token create --print-join-command
    
    • 1

    image-20220817163109548

    work节点执行kubeadm join命令
    master节点查看node信息

    4.控制节点启用pod调度

    默认情况下,出于安全原因,您的群集不会在控制节点上调度Pod。如果您希望能够在控制平面节点上调度Pod,例如用于单机Kubernetes集群进行开发,请运行

    kubectl taint nodes --all node-role.kubernetes.io/master-
    
    • 1

    5.集群以外节点控制k8s集群

    为了使kubectl在其他计算机上与集群通信,需要将管理员kubeconfig文件从控制平面节点复制到计算机上
    scp root@<control-plane-host>:/etc/kubernetes/admin.conf .
    
    • 1
    kubectl --kubeconfig ./admin.conf get nodes
    
    • 1
    注意:

    上面的示例假定为root用户启用了SSH访问。如果不是这种情况,您可以复制admin.conf文件以供其他用户访问,而scp改用该其他用户

    该admin.conf文件为用户提供了对集群的超级用户特权。该文件应谨慎使用。对于普通用户,建议生成一个唯一的凭据,将其特权列入白名单。您可以使用kubeadm alpha kubeconfig user --client-name 命令执行此操作。该命令会将KubeConfig文件打印到STDOUT,您应该将其保存到文件并分发给用户。之后,使用来将特权列入白名单kubectl create (cluster)rolebinding。

    6.删除本地集群

    使用 kubectl config delete-cluster删除对集群的本地引用。

    如果要更干净地取消配置群集,则应首先排空该节点,并确保该节点为空,然后取消配置该节点。

    删除节点

    使用适当的凭证与控制平面节点通信,请运行:
    kubectl drain  --delete-local-data --force --ignore-daemonsets
    kubectl delete node 
    
    • 1
    • 2

    然后,在要删除的节点上,重置所有kubeadm安装状态:

    kubeadm reset
    
    • 1

    重置过程不会重置或清除iptables规则或IPVS表。如果您希望重置iptables,则必须手动进行:

    iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
    
    • 1

    如果要重置IPVS表,则必须运行以下命令:

    ipvsadm -C
    
    • 1

    如果您想重新开始,只需运行kubeadm init或kubeadm join使用适当的参数即可。

    7.节点维护状态

    节点驱逐

     kubectl drain NODE
    
    • 1

    恢复调度

    kubectl uncordon NODE
    
    • 1

    r)rolebinding。

    6.删除本地集群

    使用 kubectl config delete-cluster删除对集群的本地引用。

    如果要更干净地取消配置群集,则应首先排空该节点,并确保该节点为空,然后取消配置该节点。

    删除节点

    使用适当的凭证与控制平面节点通信,请运行:
    kubectl drain  --delete-local-data --force --ignore-daemonsets
    kubectl delete node 
    
    • 1
    • 2

    然后,在要删除的节点上,重置所有kubeadm安装状态:

    kubeadm reset
    
    • 1

    重置过程不会重置或清除iptables规则或IPVS表。如果您希望重置iptables,则必须手动进行:

    iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
    
    • 1

    如果要重置IPVS表,则必须运行以下命令:

    ipvsadm -C
    
    • 1

    如果您想重新开始,只需运行kubeadm init或kubeadm join使用适当的参数即可。

    7.节点维护状态

    节点驱逐

     kubectl drain NODE
    
    • 1

    恢复调度

    kubectl uncordon NODE
    
    • 1
  • 相关阅读:
    为什么PLC与触摸屏的通信总断开?如何进行远程维护?
    2023年浙江工业大学MPA提前批招生通知
    moment.js 实现获取近一月、近三月、近一年、一月后、一年后等
    探索生成式AI的未来:Chat与Agent的较量与融合
    嵌入式学习(Day 51:ARM指令/汇编与c语言函数相互调用)
    BOM系列之history对象
    【C++学习第六讲】第一章练习题(含源代码)
    机器学习笔记之马尔可夫链蒙特卡洛方法(四)吉布斯采样
    如何将GitLab仓库同步到GitHub和Gitee?
    Java爬虫实战:API商品数据接口调用
  • 原文地址:https://blog.csdn.net/tianmingqing0806/article/details/126389489