目前18.6版本和以前的coredns都會出現超時5s的狀況,那麼爲何會出現coredns超時的狀況發生?node
在Kubernetes中,Pod訪問DNS服務器(kube-dns)的最多見方法是經過服務抽象。 所以,在嘗試解釋問題以前,瞭解服務的工做原理以及所以在Linux內核中如何實現目標網絡地址轉換(DNAT)相當重要。linux
在iptables模式下(默認狀況下),每一個服務的kube-proxy在主機網絡名稱空間的nat表中建立一些iptables規則。
讓咱們考慮在集羣中具備兩個DNS服務器實例的kube-dns服務。 相關規則以下:nginx
(1) -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES <...> (2) -A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU <...> (3) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LLLB6FGXBLX6PZF7 (4) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRVEW52VMYCOUSMZ <...> (5) -A KUBE-SEP-LLLB6FGXBLX6PZF7 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.6:53 <...> (6) -A KUBE-SEP-LRVEW52VMYCOUSMZ -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.7:53
在咱們的示例中,每一個Pod的/etc/resolv.conf中都有填充的名稱服務器10.96.0.10條目。 所以,來自Pod的DNS查找請求將發送到10.96.0.10,它是kube-dns服務的ClusterIP(虛擬IP)。
因爲(1),請求進入KUBE-SERVICE鏈,而後匹配規則(2)最後根據(3)隨機值,跳轉到(5)或(6)根據規則( 負載平衡),將請求UDP數據包的目標IPv4地址修改成DNS服務器的「實際」 IPv4地址。 這種修飾是由DNAT完成的。
10.32.0.6和10.32.0.7是Weave Net網絡中Kubernetes DNS服務器容器的IPv4地址。git
如上所示,服務(在iptables模式下)的基礎是DNAT,它由內核執行。github
DNAT的主要職責是同時更改傳出數據包的目的地,答覆數據包的源,並確保對全部後續數據包進行相同的修改。
後者嚴重依賴於鏈接跟蹤機制,也稱爲conntrack,它被實現爲內核模塊。顧名思義,conntrack會跟蹤系統中正在進行的網絡鏈接。
以一種簡化的方式,conntrack中的每一個鏈接都由兩個元組表示-一個元組用於原始請求(IP_CT_DIR_ORIGINAL),另外一個元組用於答覆(IP_CT_DIR_REPLY)。對於UDP,每一個元組都由源IP地址,源端口以及目標IP地址和目標端口組成。答覆元組包含存儲在src字段中的目標的真實地址。
例如,若是IP地址爲10.40.0.17的Pod向kube-dns的ClusterIP發送一個請求,該請求被轉換爲10.32.0.6,則將建立如下元組:web
原始:src = 10.40.0.17 dst = 10.96.0.10 sport = 53378 dport = 53 回覆:src = 10.32.0.6 dst = 10.40.0.17 sport = 53 dport = 53378
經過具備這些條目,內核能夠相應地修改任何相關數據包的目的地和源地址,而無需再次遍歷DNAT規則。此外,它將知道如何修改回覆以及應將回復發送給誰。
建立conntrack條目後,將首先對其進行確認。稍後,若是沒有已確認的conntrack條目具備相同的原始元組或回覆元組,則內核將嘗試確認該條目。
conntrack建立和DNAT的簡化流程以下所示:express
+---------------------------+ | | 爲一個給定的包建立一個conntrack,若是 | 1. nf_conntrack_in | 它並不存在;IP_CT_DIR_REPLY是 | | 反向的IP_CT_DIR_ORIGINAL元組,所以 +------------+--------------+ 回覆元組的src尚未改變。 | v +---------------------------+ | | | 2. ipt_do_table | 找到一個匹配的DNAT規則。 | | +------------+--------------+ | v +---------------------------+ | | 根據DNAT規則更新回覆元組src部分 | 3. get_unique_tuple | 使其不被任何已經確認的鏈接使用。 | | +------------+--------------+ | v +---------------------------+ | | | 4. nf_nat_packet | 根據應答元組打亂數據包的目的端口和地址。 | | +------------+--------------+ | v +----------------------------+ | | 若是沒有與相同的原始元組或應答元組確認的連 | 5. __nf_conntrack_confirm | 則確認鏈接道; | | +----------------------------+ 遞增insert_failed計數器並刪除數據包(若是在)。
當從不一樣線程經過同一套接字同時發送兩個UDP數據包時,會出現問題。
UDP是無鏈接協議,所以connect(2)syscall(與TCP相反)不會發送任何數據包,所以,在調用以後沒有建立conntrack條目。
該條目僅在發送數據包時建立。這致使如下可能:apache
一、兩個包都沒有在1中找到一個確認的conntrack。nf_conntrack_in一步。爲兩個包建立具備相同元組的兩個conntrack條目。
二、與上面的狀況相同,但一個包的conntrack條目在另外一個包調用3以前被確認。get_unique_tuple。另外一個包一般在源端口更改後獲得一個不一樣的應答元組。
三、與第一種狀況相同,可是在步驟2中選擇了具備不一樣端點的兩個不一樣規則。ipt_do_table。api
競爭的結果是相同的—其中一個包在步驟5中被丟棄。__nf_conntrack_confirm。緩存
這正是在DNS狀況下發生的狀況。 GNU C庫和musl libc都並行執行A和AAAA DNS查找。因爲競爭,內核可能會丟棄其中一個UDP數據包,所以客戶端一般會在5秒的超時後嘗試從新發送它。
值得一提的是,這個問題不只是針對Kubernetes的-任何並行發送UDP數據包的Linux多線程進程都容易出現這種競爭狀況。
另外,即便您沒有任何DNAT規則,第二場競爭也可能發生-加載nf_nat內核模塊足以啓用對get_unique_tuple的調用就足夠了。
可使用conntrack -S得到的insert_failed計數器能夠很好地指示您是否遇到此問題。
建議採起多種解決方法:禁用並行查找,禁用IPv6以免AAAA查找,使用TCP進行查找,改成在Pod的解析器配置文件中設置DNS服務器的真實IP地址,等等。不幸的是,因爲經常使用的容器基礎映像Alpine Linux使用musl libc的限制,它們中的許多不起做用。
對於Weave Net用戶來講彷佛可靠的方法是使用tc延遲DNS數據包。
另外,您可能想知道在ipvs模式下的kube-proxy是否能夠繞過這個問題。答案是否認的,由於conntrack也是在這種模式下啓用的。此外,在使用rr調度程序時,能夠在DNS流量較高的集羣中輕鬆重現第3次競爭。
不管採用哪一種解決方法,都決定在內核中修復根本緣由。
結果是如下內核補丁:
一、 「 netfilter:nf_conntrack:解決衝突以匹配conntracks」修復了第一場比賽(被接受)。
二、 「 netfilter:nf_nat:返回相同的答覆元組以匹配CT」修復了第二場比賽(等待複審)。
這兩個補丁解決了僅運行一個DNS服務器實例的羣集的問題,同時下降了其餘實例的超時命中率。
爲了在全部狀況下徹底消除問題,須要解決第三場競爭。一種可能的解決方法是在步驟5中將衝突的conntrack條目與來自同一套接字的不一樣目的地合併。__nf_conntrack_confirm。可是,這會使在該步驟中更改了目的地的數據包的先前iptables規則遍歷的結果無效。
另外一種可能的解決方案是在每一個節點上運行DNS服務器實例,並按照個人同事的建議,經過Pod查詢運行在本地節點上的DNS服務器。
結論
首先,我展現了「 DNS查找須要5秒」問題的基本細節,並揭示了罪魁禍首-Linux conntrack內核模塊,它本質上是不受歡迎的。有關模塊中也存在其餘可能的問題
方案(一):使用 TCP 協議發送 DNS 請求
經過resolv.conf的use-vc選項來開啓 TCP 協議
測試
一、修改/etc/resolv.conf文件,在最後加入一行文本:
options use-vc
二、此壓測可根據下面測試的go文件進行測試,編譯好後放進一個pod中,進行壓測:
#200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
方案(二):避免相同五元組 DNS 請求的併發
經過resolv.conf的single-request-reopen和single-request選項來避免:
single-request-reopen (glibc>=2.9) 發送 A 類型請求和 AAAA 類型請求使用不一樣的源端口。這樣兩個請求在 conntrack 表中不佔用同一個表項,從而避免衝突。
single-request (glibc>=2.10) 避免併發,改成串行發送 A 類型和 AAAA 類型請求,沒有了併發,從而也避免了衝突。
測試 single-request-reopen
修改/etc/resolv.conf文件,在最後加入一行文本:
options single-request-reopen
此壓測可根據下面測試的go文件進行測試,編譯好後放進一個pod中,進行壓測:
#200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
測試 single-request
修改/etc/resolv.conf文件,在最後加入一行文本:
options single-request
此壓測可根據下面測試的go文件進行測試,編譯好後放進一個pod中,進行壓測:
#200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
最後結果,若是你測試過,相信coredns的測試若是仍是增長使用 TCP 協議發送 DNS 請求,仍是避免相同五元組 DNS 請求的併發,都沒有顯著的解決coredns延遲的結果
那麼其實 k8s 官方也意識到了這個問題比較常見,因此也給出了 coredns 以 cache 模式做爲 daemonset 部署的解決方案
https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns/nodelocaldns
NodeLocal DNSCache 經過在集羣節點上做爲 DaemonSet 運行 dns 緩存代理來提升集羣 DNS 性能。 在當今的體系結構中,處於 ClusterFirst DNS 模式的 Pod 能夠鏈接到 kube-dns serviceIP 進行 DNS 查詢。 經過 kube-proxy 添加的 iptables 規則將其轉換爲 kube-dns/CoreDNS 端點。 藉助這種新架構,Pods 將能夠訪問在同一節點上運行的 dns 緩存代理,從而避免了 iptables DNAT 規則和鏈接跟蹤。 本地緩存代理將查詢 kube-dns 服務以獲取集羣主機名的緩存缺失(默認爲 cluster.local 後綴),並有效解決5秒延遲問題
若是本地沒有 CoreDNS 實例,則具備最高 DNS QPS 的 Pod 可能必須到另外一個節點進行解析,使用 NodeLocal DNSCache 後,擁有本地緩存將有助於改善延遲
跳過 iptables DNAT 和鏈接跟蹤將有助於減小 conntrack 競爭並避免 UDP DNS 條目填滿 conntrack 表。(常見的5s超時問題就是這個緣由形成的)
從本地緩存代理到 kube-dns 服務的鏈接能夠升級到 TCP,TCP conntrack 條目將在鏈接關閉時被刪除,而 UDP 條目必須超時(默認 nf_conntrack_udp_timeout 是 30 秒)
將 DNS 查詢從 UDP 升級到 TCP 將減小歸因於丟棄的 UDP 數據包和 DNS 超時的尾部等待時間,一般長達 30 秒(3 次重試+ 10 秒超時)。
能夠從新啓用負緩存,從而減小對 kube-dns 服務的查詢數量。
啓用 NodeLocal DNSCache 以後,這是 DNS 查詢所遵循的路徑:
該資源清單文件中包含幾個變量,其中:
PILLARDNSSERVER :表示 kube-dns 這個 Service 的 ClusterIP,能夠經過命令 kubectl get svc -n A | grep kube-dns | awk '{ print $4 }' 獲取
PILLARLOCALDNS:表示 DNSCache 本地的 IP,默認爲 169.254.20.10
PILLARDNSDOMAIN:表示集羣域,默認就是 cluster.local
另外還有兩個參數 PILLARCLUSTERDNS 和 PILLARUPSTREAMSERVERS,這兩個參數會經過鏡像 1.15.6 版本以上的去進行配置,對應的值來源於 kube-dns 的 ConfigMap 和定製的 Upstream Server 配置。直接執行以下所示的命令便可安裝:
運行nodelocaldns須要進行替換如下操做,若是下載過慢,能夠直接使用下面的yaml來使用,須要替換的話,只有10.96.0.10,這個是kube-dns service的clusterIP
wget -O nodelocaldns.yaml "https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml" && \ sed -i 's/k8s.gcr.io/zhaocheng172/g' nodelocaldns.yaml && \ sed -i 's/__PILLAR__DNS__SERVER__/10.96.0.10/g' nodelocaldns.yaml && \ sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml && \ sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml
最終替換結果
#Copyright 2018 The Kubernetes Authors. #Licensed under the Apache License, Version 2.0 (the "License"); #you may not use this file except in compliance with the License. #You may obtain a copy of the License at #http://www.apache.org/licenses/LICENSE-2.0 #Unless required by applicable law or agreed to in writing, software #distributed under the License is distributed on an "AS IS" BASIS, #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #See the License for the specific language governing permissions and #limitations under the License. apiVersion: v1 kind: ServiceAccount metadata: name: node-local-dns namespace: kube-system labels: kubernetes.io/cluster-service: "true" addonmanager.kubernetes.io/mode: Reconcile --- apiVersion: v1 kind: Service metadata: name: kube-dns-upstream namespace: kube-system labels: k8s-app: kube-dns kubernetes.io/cluster-service: "true" addonmanager.kubernetes.io/mode: Reconcile kubernetes.io/name: "KubeDNSUpstream" spec: ports: - name: dns port: 53 protocol: UDP targetPort: 53 - name: dns-tcp port: 53 protocol: TCP targetPort: 53 selector: k8s-app: kube-dns --- apiVersion: v1 kind: ConfigMap metadata: name: node-local-dns namespace: kube-system labels: addonmanager.kubernetes.io/mode: Reconcile data: Corefile: | cluster.local:53 { errors cache { success 9984 30 denial 9984 5 } reload loop bind 169.254.20.10 10.96.0.10 forward . __PILLAR__CLUSTER__DNS__ { force_tcp } prometheus :9253 health 169.254.20.10:8080 } in-addr.arpa:53 { errors cache 30 reload loop bind 169.254.20.10 10.96.0.10 forward . __PILLAR__CLUSTER__DNS__ { force_tcp } prometheus :9253 } ip6.arpa:53 { errors cache 30 reload loop bind 169.254.20.10 10.96.0.10 forward . __PILLAR__CLUSTER__DNS__ { force_tcp } prometheus :9253 } .:53 { errors cache 30 reload loop bind 169.254.20.10 10.96.0.10 forward . __PILLAR__UPSTREAM__SERVERS__ { force_tcp } prometheus :9253 } --- apiVersion: apps/v1 kind: DaemonSet metadata: name: node-local-dns namespace: kube-system labels: k8s-app: node-local-dns kubernetes.io/cluster-service: "true" addonmanager.kubernetes.io/mode: Reconcile spec: updateStrategy: rollingUpdate: maxUnavailable: 10% selector: matchLabels: k8s-app: node-local-dns template: metadata: labels: k8s-app: node-local-dns annotations: prometheus.io/port: "9253" prometheus.io/scrape: "true" spec: priorityClassName: system-node-critical serviceAccountName: node-local-dns hostNetwork: true dnsPolicy: Default # Don't use cluster DNS. tolerations: - key: "CriticalAddonsOnly" operator: "Exists" - effect: "NoExecute" operator: "Exists" - effect: "NoSchedule" operator: "Exists" containers: - name: node-cache image: zhaocheng172/k8s-dns-node-cache:1.15.13 resources: requests: cpu: 25m memory: 5Mi args: [ "-localip", "169.254.20.10,10.96.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ] securityContext: privileged: true ports: - containerPort: 53 name: dns protocol: UDP - containerPort: 53 name: dns-tcp protocol: TCP - containerPort: 9253 name: metrics protocol: TCP livenessProbe: httpGet: host: 169.254.20.10 path: /health port: 8080 initialDelaySeconds: 60 timeoutSeconds: 5 volumeMounts: - mountPath: /run/xtables.lock name: xtables-lock readOnly: false - name: config-volume mountPath: /etc/coredns - name: kube-dns-config mountPath: /etc/kube-dns volumes: - name: xtables-lock hostPath: path: /run/xtables.lock type: FileOrCreate - name: kube-dns-config configMap: name: kube-dns optional: true - name: config-volume configMap: name: node-local-dns items: - key: Corefile path: Corefile.base
能夠經過以下命令來查看對應的 Pod 是否已經啓動成功:
須要注意的是這裏使用 DaemonSet 部署 node-local-dns 使用了 hostNetwork=true,會佔用宿主機的 8080 端口,因此須要保證該端口未被佔用。
另外咱們還須要修改 kubelet 的 --cluster-dns 參數,將其指向 169.254.20.10,Daemonset 會在每一個節點建立一個網卡來綁這個 IP,Pod 向本節點這個 IP 發 DNS 請求,緩存沒有命中的時候纔會再代理到上游集羣 DNS 進行查詢。
第一種就是定製一個pod,Kubernetes Pod dnsPolicy 能夠針對每一個Pod設置DNS的策略,經過PodSpec下的dnsPolicy字段能夠指定相應的策略
這種方式能夠直接啓動一個pod,Pods將直接能夠訪問在同一節點上運行的 dns 緩存代理,從而避免了 iptables DNAT 規則和鏈接跟蹤,可是這種對於總體集羣來說並不適合,只提升了當前pod的DNScache的命中率,這種適合定製一些dns策略
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: web name: web namespace: kube-system spec: replicas: 1 selector: matchLabels: app: web strategy: {} template: metadata: creationTimestamp: null labels: app: web spec: containers: - image: nginx name: nginx dnsConfig: nameservers: - 169.254.20.10 searches: - public.svc.cluster.local - svc.cluster.local - cluster.local options: - name: ndots value: "5" dnsPolicy: None
第二種若是對於集羣來說,須要所有生效
須要替換每一個節點的clusterDNS的地址
clusterDNS: - 10.96.0.10
替換的話能夠直接使用sed直接替換,另外須要全部節點替換並重啓kubelet
sed -i 's/10.96.0.10/169.254.20.10/g' /var/lib/kubelet/config.yaml systemctl daemon-reload systemctl restart kubelet
待 node-local-dns 安裝配置完成後,咱們能夠部署一個新的 Pod 來驗證下:(test-node-local-dns.yaml)
apiVersion: v1 kind: Pod metadata: name: test-node-local-dns spec: containers: - name: local-dns image: busybox command: ["/bin/sh", "-c", "sleep 60m"]
直接部署:
$ kubectl apply -f test-node-local-dns.yaml $ kubectl exec -it test-node-local-dns /bin/sh / # cat /etc/resolv.conf nameserver 169.254.20.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5
須要注意
咱們如今已經能夠看到 nameserver 已經變成 169.254.20.10 了,固然對於以前的歷史 Pod 要想使用 node-local-dns 則須要重建全部的pod
一、linux 中glibc的 resolver 的缺省超時時間是 5s,而致使超時的緣由是內核conntrack模塊的 bug。
二、DNS client (glibc 或 musl libc) 會併發請求 A 和 AAAA 記錄,跟 DNS Server 通訊天然會先 connect (創建 fd),後面請求報文使用這個 fd 來發送,因爲 UDP 是無狀態協議, connect 時並不會發包,也就不會建立 conntrack 表項, 而併發請求的 A 和 AAAA 記錄默認使用同一個 fd 發包,send 時各自發的包它們源 Port 相同(由於用的同一個 socket 發送),當併發發包時,兩個包都尚未被插入 conntrack 表項,因此 netfilter 會爲它們分別建立 conntrack 表項,而集羣內請求 kube-dns 或 coredns 都是訪問的 CLUSTER-IP,報文最終會被 DNAT 成一個 endpoint 的 POD IP,當兩個包剛好又被 DNAT 成同一個 POD IP 時,它們的五元組就相同了,在最終插入的時候後面那個包就會被丟掉,若是 dns 的 pod 副本只有一個實例的狀況就很容易發生(始終被 DNAT 成同一個 POD IP),現象就是 dns 請求超時,client 默認策略是等待 5s 自動重試,若是重試成功,咱們看到的現象就是 dns 請求有 5s 的延時。
另外若是要想去跟蹤 DNS 的解析過程的話能夠去經過抓包來觀察具體超時的最大時間。
測試coredns+nodelocaldns的效果
咱們能夠經過Go的一個測試用例來測試DNS的解析是否獲得提高
首先安裝一個Go的環境
#wget https://dl.google.com/go/go1.12.5.linux-amd64.tar.gz #tar -zxf go1.12.5.linux-amd64.tar.gz -C /usr/local #export PATH=$PATH:/usr/local/go/bin #go version
主要是要測試 dns 服務的性能,至關於壓測工具只作域名解析的耗時時間
cat dns-test.go package main import ( "context" "flag" "fmt" "net" "sync/atomic" "time" ) var host string var connections int var duration int64 var limit int64 var timeoutCount int64 func main() { // os.Args = append(os.Args, "-host", "www.baidu.com", "-c", "200", "-d", "30", "-l", "5000") flag.StringVar(&host, "host", "", "Resolve host") flag.IntVar(&connections, "c", 100, "Connections") flag.Int64Var(&duration, "d", 0, "Duration(s)") flag.Int64Var(&limit, "l", 0, "Limit(ms)") flag.Parse() var count int64 = 0 var errCount int64 = 0 pool := make(chan interface{}, connections) exit := make(chan bool) var ( min int64 = 0 max int64 = 0 sum int64 = 0 ) go func() { time.Sleep(time.Second * time.Duration(duration)) exit <- true }() endD: for { select { case pool <- nil: go func() { defer func() { <-pool }() resolver := &net.Resolver{} now := time.Now() _, err := resolver.LookupIPAddr(context.Background(), host) use := time.Since(now).Nanoseconds() / int64(time.Millisecond) if min == 0 || use < min { min = use } if use > max { max = use } sum += use if limit > 0 && use >= limit { timeoutCount++ } atomic.AddInt64(&count, 1) if err != nil { fmt.Println(err.Error()) atomic.AddInt64(&errCount, 1) } }() case <-exit: break endD } } fmt.Printf("request count:%d\nerror count:%d\n", count, errCount) fmt.Printf("request time:min(%dms) max(%dms) avg(%dms) timeout(%dn)\n", min, max, sum/count, timeoutCount) }
直接go build,會有一個dns-test的二進制文件
將此文件放到pod裏面#kubectl cp /root/dns-test web-7f5df76d5f-r76xx:/root -n kube-system
下面來進行測試
進行壓測
200個併發,持續30秒,記錄超過5s的請求個數
/dns -host {service}.{namespace} -c 200 -d 30 -l 5000
結果以下:
1.14.3版本原生集羣不加參數測試默認使用iptables性能方面可能不是那麼好,不過已經沒有5s延遲狀況發生,最高耗時2.9s
1.18.6原生集羣不加參數測試
結論沒有延遲操做最大耗時爲0.5s,默認採用ipvs,效率很是高
最後總結:經過測試結果獲得如下結論1.14.3集羣使用coredns+nodelocaldns配合使用避免相同五元組 DNS 請求的併發,增長options single-request-reopen,最大耗時下降到2.25s左右,不會出現5s超時狀況,效果最好1.18.6集羣使用coredns+nodelocaldns不加參數測試最大耗時下降到0.53s左右,效率明顯提高,效果最好