大規模微服務利器:eBPF - Kubernetes

hi, 大家好,微服務,雲原生近來大熱,在企業積極進行數字化轉型,全面提升效率的今天,幾乎無人否認雲原生代表着雲計算的 “下一個時代”,IT 大廠們都不約而同的將其視爲未來雲應用的發展方向,從網絡方向來看,eBPF 可能會成爲“爲雲而生” 下一代網絡技術,今天分享一篇經典 eBPF 文章,希望大家喜歡。

譯者序

本文翻譯自 2020 年 Daniel Borkmann 在 KubeCon 的一篇分享: eBPF and Kubernetes: Little Helper Minions for Scaling Microservices, 視頻見油管。翻譯已獲得 Daniel 授權。

Daniel 是 eBPF 兩位 maintainer 之一,目前在 eBPF commits 榜單上排名第一,也是 Cilium 的核心開發者之一。

本文內容的時間跨度有 8 年,覆蓋了 eBPF 發展的整個歷史,非常值得一讀。時間限制, Daniel 很多地方只是點到,沒有展開。譯文中加了一些延展閱讀,有需要的同學可以參考。

由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。

http://arthurchiao.art/blog/ebpf-and-k8s-zh

以下是譯文。

1 eBPF 正在吞噬世界

1.1 Kubernetes 已經是雲操作系統

Kubernetes 正在吞噬世界(eating the world)。越來越多的企業開始遷移到容器平臺上 ,而 Kubernetes 已經是公認的雲操作系統(Cloud OS)。從技術層面來說,

  1. Linux 內核是一切的堅實基礎,例如,內核提供了 cgroup、namespace 等特性。

  2. Kubernetes CNI 插件串聯起了關鍵路徑(critical path)上的組件。例如,從網絡的 視角看,包括,

1.2 兩個清晰的容器技術趨勢

今天我們能清晰地看到兩個技術發展趨勢:

  1. 容器的部署密度越來越高(increasing Pod density)。

  2. 容器的生命週期越來越短(decreasing Pod lifespan)。甚至短到秒級或毫秒級。

大家有興趣的可以查閱相關調查:

2 內核面臨的挑戰

從操作系統內核的角度看,我們面臨很多挑戰。

2.1 複雜度性不斷增長,性能和可擴展性新需求

內核,或者說通用內核(general kernel),必須在子系統複雜度不斷增長( increasing complexity of kernel subsystems)的前提下,滿足這些性能和可擴展性 需求(performance & scalability requirements)。

2.2 永遠保持後向兼容

Linus torvalds 的名言大家都知道:never break user space。

這對用戶來說是好事,但對內核開發者來說意味着:我們必須保證在引入新代碼時,五年前 甚至十幾年前的老代碼仍然能正常工作。

顯然,這會使原本就複雜的內核變得更加複雜,對於網絡來說,這意味着快速收發包路徑 (fast path)將受到影響。

2.3 Feature creeping normality

開發者和用戶不斷往內核加入新功能,導致內核非常複雜,現在已經沒有一個人能理解所有東西了。

Wikipedia 對 creeping normality 的定義:

Def. creeping normality: … is a process by which a major change can be accepted as normal and acceptable if it happens slowly through small, often unnoticeable, increments of change. The change could otherwise be regarded as objectionable if it took place in a single step or short period.

應用到這裏,意思就是:內核不允許一次性引入非常大的改動,只能將它們拆 分成數量衆多的小 patch,每次合併的 patch 保證系統後向兼容,並且對系統的影響非 常小。

來看 Linus torvalds 的原話:

Linus Torvalds on crazy new kernel features:

So I can work with crazy people, that’s not the problem. They just need to sell their crazy stuff to me using non-crazy arguments, and in small and well-defined pieces. When I ask for killer features, I want them to lull me into a safe and cozy world where the stuff they are pushing is actually useful to mainline people first.

In other words, every new crazy feature should be hidden in a nice solid “Trojan Horse” gift: something that looks obviously good at first sight.

Linus Torvalds,

https://lore.kernel.org/lkml/alpine.LFD.2.00.1001251002430.3574@localhost.localdomain/

3 eBPF 降世:重新定義數據平面(datapth)

這就是我們最開始想將 eBPF 合併到內核時遇到的問題:改動太大,功能太新(a crazy new kernel feature)。

但是,eBPF 帶來的好處也是無與倫比的。

首先,從長期看,eBPF 這項新功能會減少未來的 feature creeping normality。因爲用戶或開發者希望內核實現的功能,以後不需要再通過改內核的方式來實現了。只需要一段 eBPF 代碼,實時動態加載到內核就行了。

其次,因爲 eBPF,內核也不會再引入那些影響 fast path 的蹩腳甚至 hardcode 代碼 ,從而也避免了性能的下降。

第三,eBPF 還使得內核完全可編程,安全地可編程(fully and safely programmable ),用戶編寫的 eBPF 程序不會導致內核 crash。另外,eBPF 設計用來解決真實世界 中的線上問題,而且我們現在仍然在堅守這個初衷。

4 eBPF 長什麼樣,怎麼用?Cilium eBPF networking 案例研究

eBPF 程序長什麼樣?如下圖所示,和 C 語言差不多,一般由用戶空間 application 或 agent 來生成。

4.1 Cilium eBPF 流程

下面我們將看看 Cilium 是如何用 eBPF 實現容器網絡方案的。

如上圖所示,幾個步驟:

  1. Cilium agent 生成 eBPF 程序。

  2. 用 LLVM 編譯 eBPF 程序,生成 eBPF 對象文件(object file,*.o)。

  3. 用 eBPF loader 將對象文件加載到 Linux 內核。

  4. 校驗器(verifier)對 eBPF 指令會進行合法性驗證,以確保程序是安全的,例如 ,無非法內存訪問、不會 crash 內核、不會有無限循環等。

  5. 對象文件被即時編譯(JIT)爲能直接在底層平臺(例如 x86)運行的 native code。

  6. 如果要在內核和用戶態之間共享狀態,BPF 程序可以使用 BPF map,這種一種共享存儲 ,BPF 側和用戶側都可以訪問。

  7. BPF 程序就緒,等待事件觸發其執行。對於這個例子,就是有數據包到達網絡設備時,觸發 BPF 程序的執行。

  8. BPF 程序對收到的包進行處理,例如 mangle。最後返回一個裁決(verdict)結果。

  9. 根據裁決結果,如果是 DROP,這個包將被丟棄;如果是 PASS,包會被送到更網絡棧的 更上層繼續處理;如果是重定向,就發送給其他設備。

4.2 eBPF 特點

  1. 最重要的一點:不能 crash 內核。

  2. 執行起來,與內核模塊(kernel module)一樣快。

  3. 提供穩定的 API。

這意味着什麼?簡單來說,如果一段 BPF 程序能在老內核上執行,那它一定也能繼續在新 內核上執行,而無需做任何修改。

這就像是內核空間與用戶空間的契約,內核保證對用戶空間應用的兼容性,類似地,內核也 會保證 eBPF 程序的兼容性。

5 溫故:kube-proxy 包轉發路徑

從網絡角度看,使用傳統的 kube-proxy 處理 Kubernetes Service 時,包在內核中的 轉發路徑是怎樣的?如下圖所示:

步驟:

  1. 網卡收到一個包(通過 DMA 放到 ring-buffer)。

  2. 包經過 XDP hook 點。

  3. 內核給包分配內存,此時纔有了大家熟悉的 skb(包的內核結構體表示),然後 送到內核協議棧。

  4. 包經過 GRO 處理,對分片包進行重組。

  5. 包進入 tc(traffic control)的 ingress hook。接下來,所有橙色的框都是 Netfilter 處理點。

  6. Netfilter:在 PREROUTING hook 點處理 raw table 裏的 iptables 規則。

  7. 包經過內核的連接跟蹤(conntrack)模塊。

  8. Netfilter:在 PREROUTING hook 點處理 mangle table 的 iptables 規則。

  9. Netfilter:在 PREROUTING hook 點處理 nat table 的 iptables 規則。

  10. 進行路由判斷(FIB:Forwarding Information Base,路由條目的內核表示,譯者注) 。接下來又是四個 Netfilter 處理點。

  11. Netfilter:在 FORWARD hook 點處理 mangle table 裏的 iptables 規則。

  12. Netfilter:在 FORWARD hook 點處理 filter table 裏的 iptables 規則。

  13. Netfilter:在 POSTROUTING hook 點處理 mangle table 裏的 iptables 規則。

  14. Netfilter:在 POSTROUTING hook 點處理 nat table 裏的 iptables 規則。

  15. 包到達 TC egress hook 點,會進行出方向(egress)的判斷,例如判斷這個包是到本 地設備,還是到主機外。

  16. 對大包進行分片。根據 step 15 判斷的結果,這個包接下來可能會:

  17. 發送到一個本機 veth 設備,或者一個本機 service endpoint,

  18. 或者,如果目的 IP 是主機外,就通過網卡發出去。

相關閱讀,有助於理解以上過程:

  1. Cracking Kubernetes Node Proxy (aka kube-proxy)

  2. (譯) 深入理解 iptables 和 netfilter 架構

  3. 連接跟蹤(conntrack):原理、應用及 Linux 內核實現

  4. (譯) 深入理解 Cilium 的 eBPF 收發包路徑(datapath)

譯者注。

6 知新:Cilium eBPF 包轉發路徑

作爲對比,再來看下 Cilium eBPF 中的包轉發路徑:

建議和 (譯) 深入理解 Cilium 的 eBPF 收發包路徑(datapath) 對照看。

譯者注。

對比可以看出,Cilium eBPF datapath 做了短路處理:從 tc ingress 直接 shortcut 到 tc egress,節省了 9 箇中間步驟(總共 17 個)。更重要的是:這個 datapath 繞過了 整個 Netfilter 框架(橘黃色的框們),Netfilter 在大流量情況下性能是很差的。

去掉那些不用的框之後,Cilium eBPF datapath 長這樣:

Cilium/eBPF 還能走的更遠。例如,如果包的目的端是另一臺主機上的 service endpoint,那你可以直接在 XDP 框中完成包的重定向(收包 1->2,在步驟 2 中對包 進行修改,再通過 2->1 發送出去),將其發送出去,如下圖所示:

可以看到,這種情況下包都沒有進入內核協議棧(準確地說,都沒有創建 skb)就被轉 發出去了,性能可想而知。

XDP 是 eXpress DataPath 的縮寫,支持在網卡驅動中運行 eBPF 代碼,而無需將包送 到複雜的協議棧進行處理,因此處理代價很小,速度極快。

7 eBPF 年鑑

eBPF 是如何誕生的呢?我最初開始講起。這裏 “最初” 我指的是 2013 年之前。

2013

前浪工具和子系統

回顧一下當時的 “SDN” 藍圖。

  1. 當時有 OpenvSwitch(OVS)、tc(Traffic control),以及內核中的 Netfilter 子系 統(包括 iptablesipvsnftalbes 工具),可以用這些工具對 datapath 進行 “編程”:。

  2. BPF 當時用於 tcpdump,在內核中儘量前面的位置抓包,它不會 crash 內核;此 外,它還用於 seccomp,對系統調用進行過濾(system call filtering),但當時 使用的非常受限,遠不是今天我們已經在用的樣子。

  3. 此外就是前面提到的 feature creeping 問題,以及 tc 和 netfilter 的代碼重複問題,因爲這兩個子系統是競爭關係。

  4. OVS 當時被認爲是內核中最先進的數據平面,但它最大的問題是:與內核中其他網 絡模塊的集成不好【譯者注 1】。此外,很多核心的內核開發者也比較牴觸 OVS,覺得它很怪。

【譯者注 1】

例如,OVS 的 internal port、patch port 用 tcpdump 都是 抓不到包的,排障非常不方便。

eBPF 與前浪的區別

對比 eBPF 和這些已經存在很多年的工具:

  1. tc、OVS、netfilter 可以對 datapath 進行 “編程”:但前提是 datapath 知道你想做什 麼(but only if the datapath knows what you want to do)。
  1. eBPF 能夠讓你創建新的 datapath(eBPF lets you create the datapath instead)。
  • eBPF 就是內核本身的代碼,想象空間無限,並且熱加載到內核;換句話說,一旦加 載到內核,內核的行爲就變了。

  • 在 eBPF 之前,改變內核行爲這件事情,只能通過修改內核再重新編譯,或者開發內 核模塊才能實現。

譯者注

eBPF:第一個(巨型)patch

最終這個 patch 被拒絕了。

被拒的另外一個原因是前面提到的,沒有遵循 “大改動小提交” 原則,全部代碼放到了一個 patch。Linus 會瘋的。

2014

第一個 eBPF patch 合併到內核

我們也從那時開始,順理成章地成爲了 eBPF 的 maintainer。

Kubernetes 提交第一個 commit

巧合的是,對後來影響深遠的 Kubernetes,也在這一年提交了第一個 commit:

2015

eBPF 分成兩個方向:networking & tracing

到了 2015 年,eBPF 開發分成了兩個方向:

eBPF backend 合併到 LLVM 3.7

這一年的一個重要里程碑是 eBPF backend 合併到了 upstream LLVM 編譯器套件,因此你 現在才能用 clang 編譯 eBPF 代碼。

支持將 eBPF attach 到 kprobes

這是 tracing 的第一個使用案例。

Alexei 主要負責 tracing 部分,他添加了一個 patch,支持加載 eBPF 用來做 tracing, 能獲取系統的觀測數據。

通過 cls_bpf,tc 變得完全可編程

我主要負責 networking 部分,使 tc 子系統可編程,這樣我們就能用 eBPF 來靈活的對 datapath 進行編程,獲得一個高性能 datapath。

爲 tc 添加了一個 lockless ingress & egress hook 點

譯註:可參考:

  • 深入理解 tc ebpf 的 direct-action (da) 模式(2020)

  • 爲容器時代設計的高級 eBPF 內核特性(FOSDEM, 2021)

添加了很多 verifer 和 eBPF 輔助代碼(helper)

使用更方便。

bcc 項目發佈

作爲 tracing frontend for eBPF。

2016

eBPF 添加了一個新 fast path:XDP

Cilium 項目發佈

Cilium 最開始的目標是 docker 網絡解決方案。

2017

eBPF 開始大規模應用於生產環境

2016 ~ 2017 年,eBPF 開始應用於生產環境:

  1. Netflix on eBPF for tracing: ‘Linux BPF superpowers’

  2. Facebook 公佈了生產環境 XDP+eBPF 使用案例(DDoS & LB)

  1. Cloudflare 將 XDP+BPF 集成到了它們的 DDoS mitigation 產品。

譯者注:基於 XDP/eBPF 的 L4LB 原理都是類似的,簡單來說,

  1. 通過 BGP 宣告 VIP

  2. 通過 ECMP 做物理鏈路高可用

  3. 通過 XDP/eBPF 代碼做重定向,將請求轉發到後端(VIP -> Backend)

對此感興趣可參考入門級介紹:L4LB for Kubernetes: Theory and Practice with Cilium+BGP+ECMP

2017 ~ 2018

eBPF 成爲內核獨立子系統

隨着 eBPF 社區的發展,feature 和 patch 越來越多,爲了管理這些 patch,Alexei、我和 networking 的一位 maintainer David Miller 經過討論,決定將 eBPF 作爲獨立的內核子 系統。

kTLS & eBPF

kTLS & eBPF for introspection and ability for in-kernel TLS policy enforcement

kTLS 是將 TLS 處理 offload 到內核,例如,將加解密過程從 openssl 下放到內核進 行,以使得內核具備更強的可觀測性(gain visibility)。

有了 kTLS,就可以用 eBPF 查看數據和狀態,在內核應用安全策略。 目前 openssl 已經完全原生支持這個功能。

bpftool & libbpf

爲了檢查內核內 eBPF 的狀態(introspection)、查看內核加載了哪些 BPF 程序等, 我們添加了一個新工具 bpftool。現在這個工具已經功能非常強大了。

同樣,爲了方便用戶空間應用使用 eBPF,我們提供了用戶空間 API(user space API for applications) libbpf。這是一個 C 庫,接管了所有加載工作,這樣用戶就不需要 自己處理複雜的加載過程了。

BPF to BPF function calls

增加了一個 BPF 函數調用另一個 BPF 函數的支持,使得 BPF 程序的編寫更加靈活。

2018

Cilium 1.0 發佈

這標誌着 BPF 革命之火燃燒到了 Kubernetes networking & security 領域。

Cilium 此時支持的功能:

BTF(Byte Type Format)

內核添加了一個稱爲 BTF 的組件。這是一種元數據格式,和 DWARF 這樣的 debugging data 類似。但 BTF 的 size 要小的多,而更重要的是,有史以來,內核第一次變得可自 描述了(self-descriptive)。什麼意思?

想象一下當前正在運行中的內核,它內置了自己的數據格式(its own data format) 和內部數據結構(internal structures),你能用工具來查看這些東西(you can introspect them)。還是不太懂?這麼說吧,BTF 是後來的 “一次編譯、到處運行”、 熱補丁(live-patching)、BPF global data 處理等等所有這些 BPF 特性的基礎。

新的特性不斷加入,它們都依賴 BTF 提供富元數據(rich metadata)這個基礎。

更多 BTF 內容,可參考 (譯) Cilium:BPF 和 XDP 參考指南(2019)

譯者注

Linux Plumbers 會議開闢 BPF/XDP 主題

這一年,Linux Plumbers 會議第一次開闢了專門討論 BPF/XDP 的微型分會,我們 一起組織這場會議。其中,Networking Track 一半以上的議題都涉及 BPF 和 XDP 主題,因爲這是一個非常振奮人心的特性,越來越多的人用它來解決實際問題。

新 socket 類型:AF_XDP

內核添加了一個新 socket 類型 AF_XDP。它提供的能力是:在零拷貝( zero-copy)的前提下將包從網卡驅動送到用戶空間。

回憶前面的內容,數據包到達網卡後,先經過 XDP,然後才爲這個包分配內存。因此在 XDP 層直接將包送到用戶態是無需拷貝的。

譯者注

AF_XDP 提供的能力與 DPDK 有點類似,不過

而且由於複用了內核基礎設施,所有的網絡管理工具還都是可以用的,因此非常方便, 而 DPDK 這種 bypass 內核的方案導致絕大大部分現有工具都用不了了。

由於所有這些操作都是發生在 XDP 層的,因此它稱爲 AF_XDP。插入到這裏的 BPF 代碼 能直接將包送到 socket。

bpffilter

開始了 bpffilter prototype,作用是通過用戶空間驅動(userspace driver),將 iptables 規則轉換成 eBPF 代碼。

這是將 iptables 轉換成 eBPF 的第一次嘗試,整個過程對用戶都是無感知的,其中的某些 組件現在還在用,用於在其他方面擴展內核的功能。

2018 ~ 2019

bpftrace

Brendan 發佈了 bpftrace 工具,作爲 DTrace 2.0 for Linux。

BPF 專著《BPF Performance Tools》

Berendan 寫了一本 800 多頁的 BPF 書。

Cilium 1.6 發佈

第一次支持完全乾掉基於 iptables 的 kube-proxy,全部功能基於 eBPF。

這個版本其實是有問題的,例如 1.6 發佈之後我們發現 externalIPs 的實現是有問題 ,社區在後面的版本修復了這個問題。在修復之前,還是得用 kube-proxy:https://github.com/cilium/cilium/issues/9285

譯者注

BPF live-patching

添加了一些內核新特性,例如尾調用(tail call),這使得 eBPF 核心基礎 設施第一次實現了熱加載。這個功能幫我們極大地優化了 datapath。

另一個重要功能是 BPF trampolines,這裏就不展開了,感興趣的可以搜索相關資料,我只 能說這是另一個振奮人心的技術。

第一次 bpfconf:受邀請才能參加的 BPF 內核專家會議

如題,這是 BPF 內核專家交換想法和討論問題的會議。與 Linux Plumbers 會議互補。

BPF backend 合併到 GCC

前面提到,BPF backend 很早就合併到 LLVM/Clang,現在,它終於合併到 GCC 了。至此,GCC 和 LLVM 這兩個最主要的編譯器套件都支持了 BPF backend。

此外,BPF 開始支持有限循環(bounded loops),在此之前,是不支持循環的,以防止程 序無限執行。

2019 ~ 2020

不知疲倦的增長和 eBPF 的第三個方向:Linux security modules

8 eBPF:過去 50 年操作系統最大的變革

Brendan 說在他的職業生涯中,eBPF 是他見過的操作系統中最大的變革之一(one of the biggest operating system changes),他爲能身處其中而感到非常興奮。

我接下來只能用數字證明:Brendan 的興奮是沒錯的。

9 eBPF 數字榜單(截至 2020.07)

eBPF 內核社區截至 7 月份的一些數據:

毫無疑問,這是內核裏發展最快的子系統!

10 業界趨勢

注意貢獻榜排名第一的就是演講者本人,譯者注。

列舉幾個生產環境大規模使用 BPF 的大廠:

上圖中,右下角是前 Netfilter 維護者 Rusty Russel 說的一句,業界對 eBPF 的受認可程度可窺一斑。

11 eBPF 革命:燃燒到 Kubernetes 社區

eBPF 已無處不在,而你還在使用 iptables?

11.1 幹掉 kube-proxy/iptables

不用再忍受 iptables 複雜冗長的規則和差勁的性能了,以前沒得選,現在你可以做個好人:

$ kubectl -n kube-system delete ds kube-proxy

作爲例子,我們來看看 Cilium 是怎麼做 Service 的負載均衡的。

Service 細節實現可參考 Cracking Kubernetes Node Proxy (aka kube-proxy)。

譯者注。

11.2 Cilium 的 Service load balancing 設計

如上圖所示,主要涉及兩部分:

  1. 在 socket 層運行的 BPF 程序

  2. 在 XDP 和 tc 層運行的 BPF 程序

東西向流量

我們先來看 socker 層。

如上圖所示,

Socket 層的 BPF 程序主要處理 Cilium 節點的東西向流量(E-W)。

這裏實現的好處:性能更高。

可以看出,應用對這種攔截和重定向是無感知的(符合 k8s Service 的設計)。

南北向流量

再來看從 k8s 集羣外進入節點,或者從節點出 k8s 集羣的流量(external traffic), 即南北向流量(N-S):

區分集羣外流量的一個原因是:Pod IP 很多情況下都是不可路由的(與跨主機選用的網 絡方案有關),只在集羣內有效,即,集羣外訪問 Pod IP 是不通的。

因此,如果 Pod 流量直接從 node 出宿主機,必須確保它能正常回來。而 node IP 一般都是全局可達的,集羣外也可以訪問,所以常見的解決方案就是:在 Pod 通過 node 出集羣時,對其進行 SNAT,將源 IP 地址換成 node IP 地址;應答包回來時,再進行相 反的 DNAT,這樣包就能回到 Pod 了。

譯者注

如上圖所示,集羣外來的流量到達 node 時,由 XDP 和 tc 層的 BPF 程序進行處理, 它們做的事情與 socket 層的差不多,將 Service 的 IP:Port 映射到後端的 PodIP:Port,如果 backend pod 不在本 node,就通過網絡再發出去。發出去的流程我們 在前面 Cilium eBPF 包轉發路徑 講過了。

這裏 BPF 做的事情:執行 DNAT。這個功能可以在 XDP 層做,也可以在 TC 層做,但 在 XDP 層代價更小,性能也更高。

總結起來,這裏的核心理念就是:

  1. 將東西向流量放在離 socket 層儘量近的地方做。

  2. 將南北向流量放在離驅動(XDP 和 tc)層儘量近的地方做。

11.3 XDP/eBPF vs kube-proxy 性能對比

網絡吞吐

測試環境:兩臺物理節點,一個發包,一個收包,收到的包做 Service loadbalancing 轉發給後端 Pods。

可以看出:

  1. Cilium XDP eBPF 模式能處理接收到的全部 10Mpps(packets per second)。

  2. Cilium tc eBPF 模式能處理 3.5Mpps。

  3. kube-proxy iptables 只能處理 2.3Mpps,因爲它的 hook 點在收發包路徑上更後面的位置。

  4. kube-proxy ipvs 模式這裏表現更差,它相比 iptables 的優勢要在 backend 數量很多的時候才能體現出來。

CPU 利用率

我們生成了 1Mpps、2Mpps 和 4Mpps 流量,空閒 CPU 佔比(可以被應用使用的 CPU)結果如下:

結論與上面吞吐類似。

12 eBPF 和 Kubernetes:未來展望

“The Linux kernel continues its march towards becoming BPF runtime-powered microkernel.”

“Linux 內核繼續朝着成爲 BPF runtime-powered microkernel 而前進”。這是一個非 常有趣的思考角度。

BPF will replace Linux.

我們的目標是星辰大海,與此相比,kube-proxy replacement 只是最微不足道的開端。

13 結束語


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xfZ-X4elBBnsG2xnpAB9ow