整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          64 位字節長度的銀行余額“對任何人來說都足夠了嗎”? 譯文

          640K 對任何人來說都應該足夠了。
          據說是比爾·蓋茨在 1981 年左右說的。


          最近,在 TigerBeetle,我們決定使用 128 位整數來存儲所有財務金額和余額,不再使用 64 位整數。雖然有些人可能會認為 64 位整數(可以存儲從 0 到 2^64 的整數)足以計算地球上的沙粒,但我們意識到,如果我們想充分存儲各種交易,就需要超越這個限制。讓我們來找出原因。

          我們如何代表金錢?

          為了表示數字(并能夠用它們進行數學運算),計算機需要用二進制系統對數字進行編碼,根據數字的范圍和類型,需要一定數量的位(每個位可以是 0 或 1)。例如,從 -128 到 127 的整數(整數)僅用 8 位即可表示,但如果我們不需要負數,我們可以使用相同的位來表示從 0 到 255 的任何整數,這就是一個字節!數字越大,需要的位就越多,例如,最常見的是 16 位、32 位和 64 位數字。

          您可能已經注意到,我們討論的錢是整數,而不是十進制數或美分。小數的情況就變得更加復雜,可以使用浮點數進行編碼。雖然二進制浮點數可能適合其他計算,但它們無法準確表達十進制數。當我們嘗試將?以十進制表示為 0.33333… 時,我們人類會遇到同樣的問題,而計算機必須將1??? 以二進制表示!

          >>> 1.0 / 10

          0.10000000000000001

          隨著時間的流逝,“一分錢的零頭”會累積成很多,浮點數對金融來說是一場災難!

          因此,在 TigerBeetle 中,我們不使用分數或小數,每個賬本都表示為用戶定義的最小整數因子的倍數。例如,您可以將美元表示為美分的倍數,然后 1.00 美元的交易可以描述為 100 美分。即使是非十進制貨幣系統也可以更好地表示為共同因子的倍數。

          令人驚訝的是,我們也不使用負數(您可能遇到過僅存儲單個正/負余額的軟件分類賬)。相反,我們保留兩個單獨的嚴格正整數金額:一個用于借方,另一個用于貸方。這不僅避免了處理負數的負擔(例如溢出或下溢的無數特定于語言的環繞后果),但最重要的是通過顯示與借方和貸方不斷增加的余額相關的交易量來保留信息。當您需要獲取凈余額時,可以相應地減去兩個余額,并將凈額顯示為單個正數或負數。

          那么,為什么我們需要 128 位整數?

          回到將 1.00 美元表示為 100 美分的示例。在這種情況下,64 位整數可以表示接近 184.5 千萬億美元。雖然這對很多人來說可能不是問題,但當需要表示小于一美分的數值時,64 位整數的上限就會受到限制。添加更多小數位會大大縮小這個范圍。

          出于同樣的原因,數字貨幣是 128 位余額的另一個用例,其中最小金額可以表示為微美分 (10-6) 的數量級……甚至更小。雖然這是 TigerBeetle 支持的一個引人注目的用例,但我們發現各種其他應用程序也受益于 128 位余額。

          讓我們再思考一下 0.01 美元太大而無法代表某種東西的價值的情況。

          例如,在許多國家,一加侖/升汽油的價格要求小數點后三位,股票市場已經要求以百分之一美分的增量定價,比如0.0001。

          或者,在高頻小額支付的經濟中,也需要更高的精度和規模。堅持使用 64 位值會對現實世界的需求施加人為限制,或迫使應用程序在不同的賬本中處理同一種貨幣的不同規模,通過將金額小心地分割到多個“美元”和“微美元”賬戶中,只是因為單個 64 位余額不足以覆蓋代表數十億美元交易的許多小額支付所需的整個精度和規模范圍。

          能夠很好地(大規模地)計算的數據庫的價值也不限于金錢。TigerBeetle 的設計目的不僅是計算金錢,還包括任何可以使用復式記賬法建模的東西。例如,計算庫存物品、API 調用頻率,甚至千瓦時電量。而這些東西都不需要像金錢一樣運作,也不需要受到相同的限制。

          面向未來的會計。

          關于金額和余額上限的另一件事是,雖然單筆交易金額似乎不太可能超過萬億或千萬億的數量級,但賬戶余額會隨著時間的推移而累積。對于長期運行的系統,一個賬戶可能會在幾年內交易如此多的金額,因此一次轉賬也必須能夠將整個余額從一個賬戶轉移到另一個賬戶。這是我們遇到的一個問題,因為我們考慮是否要轉移到 128 位交易金額和/或僅 128 位賬戶余額。

          最后,即使是最意想不到的事件(例如惡性通貨膨脹)也可能將貨幣推向 64 位整數的上限,從而要求其放棄美分并去掉沒有實際用途的零。

          你的數據庫模式能夠經受住這種考驗嗎?

          我們可能無法直觀地了解 128 位整數有多大。不僅僅是 64 位的兩倍;它實際上是 2^64 倍!從這個角度來看,如果我們以微美分的規模對賬本進行編碼,64 位整數不足以處理 100 萬億美元的賬單。但是,使用 128 位整數,我們應該能夠在一千年內每秒執行 100 萬次相同值的轉賬,并且仍然不會達到賬戶余額限制。

            1.000e20  // one hundred trillion at micro-cent scale

          x 1.000e6 // 1 million transfers per second

          x 3.154e7 // the number of seconds in a year

          x 1.000e3 // a thousand years

          ------------

          = 3.154e36 // less than 2^128 ≈ 3.4e38

          讓我們做一些餐巾紙數學題吧!

          BigInteger 帶來了巨大的責任。

          現代處理器架構(例如 x86-64 和 ARM64)可以處理涉及 64 位值的算術運算,但如果我們理解正確的話,它們并不總是具有用于本機 128 位計算的特定指令集。處理 128 位操作數時,任務可能必須分割為 CPU 可以執行的 64 位部分。因此,我們考慮了與 64 位整數可能的單指令執行相比,128 位算術是否可能要求更高。

          下表比較了為 64 位和 128 位操作數生成的 x86_64 機器代碼。別擔心,您不需要是匯編專家就能明白這一點!只需注意,編譯器可以將大多數操作優化為一系列簡單的 CPU 指令,例如進位和和借位減法。這意味著使用 128 位金額的成本開銷對 TigerBeetle 來說并不重要。

          +-----------+--------------------+------------------------+

          | Operation | 64-bit operands | 128-bit operands |

          +-----------+--------------------+------------------------+

          | a + b | mov rax, rdi | mov rax, rdi |

          | | add rax, rdx | add rax, rdx |

          | | ret | adc rsi, rcx |

          | | | mov rdx, rsi |

          | | | ret |

          +-----------+--------------------+------------------------+

          | a - b | mov rax, rdi | mov rax, rdi |

          | | sub rax, rsi | sub rax, rdx |

          | | ret | sbb rsi, rcx |

          | | | mov rdx, rsi |

          | | | ret |

          +-----------+--------------------+------------------------+

          | a * b | mov rax, rdi | mulx r8, rax, rdi |

          | | imul rax, rsi | imul rsi, rdx |

          | | ret | imul rcx, rdi |

          | | | add rcx, rsi |

          | | | add r8, rcx |

          | | | mov rdx, r8 |

          | | | ret |

          +-----------+--------------------+------------------------+

          | a / b | mov rax, rdi | psuh rax |

          | | xor edx, edx | call __udivti3@PLT |

          | | div rsi | pop rcx |

          | | ret | ret |

          +-----------+--------------------+------------------------+

          | a == b | cmp rdi, rsi | xor rsi, rcx |

          | | sete aj | xor rdi, rdx |

          | | ret | or rdi, rsi |

          | | | sete al |

          | | | ret |

          +-----------+--------------------+------------------------+

          1. 為簡單起見,此匯編代碼省略了我們始終為 TigerBeetle 啟用的檢查算術邊界檢查和恐慌。2
          . 128 位除法無法表示為 64 位指令序列,需要通過軟件實現。

          作為這一變化的一部分,我們還必須考慮所有客戶端,因為 TigerBeetle 需要將其 API 公開給許多不同的編程語言,而這些語言并不總是支持 128 位整數。我們為其提供客戶端的主流語言目前需要使用任意精度整數(又名BigInteger)來對 128 位整數進行數學運算。唯一的例外是 .Net,它最近在 .Net 7.0 中添加了對 Int128 和 U Int128 數據類型的支持(向 DotNet 團隊致敬!)。

          使用 BigInteger 會帶來額外的開銷,因為它們不是作為固定大小的 128 位值處理,而是作為可變長度的字節數組在堆中分配。此外,算術運算在運行時由軟件模擬,這意味著它們無法充分利用如果編譯器知道它要處理的數字類型,那么可能實現的優化。嘿,Java、Go,甚至C#,我在看著你。

          為了減輕客戶端的成本(當然,也為了忠于我們的TigerStyle),我們將所有 128 位值(例如 ID、金額等)存儲并公開為一對堆棧分配的 64 位整數(JavaScript 除外,因為它也不支持 64 位數字)。盡管編程語言不了解這種原始類型,也無法對其進行算術運算,但我們提供了一組輔助函數,用于在每個生態系統中現有的慣用替代方案之間進行轉換(例如 BigInteger、字節數組、UUID)。

          我們的 API 旨在實現非侵入式,讓每個應用程序都可以自由選擇使用 BigInteger 或通過任何最合理的第三方數值庫處理 128 位值。我們希望盡可能提供出色的高性能低級原語,同時盡量減少“修飾”,同時又不剝奪更高層用戶的自由。

          結論

          TigerBeetle 專為新時代而設計,在這個時代,金融交易更加精確、更加頻繁。新時代已經開始,日常生活中充滿了這樣的例子:64 位余額“應該足夠了!”這種情況不會持續太久。128 位……甚至更遠!



          作者:Rafael Batiati

          出處:https://tigerbeetle.com/blog/2023-09-19-64-bit-bank-balances-ought-to-be-enough-for-anybody

          一個類開始

          我們從一個簡單類開始說起:

          package example.classLifecicle;public class SimpleClass {	public static void main(String[] args) {
          SimpleClass ins = new SimpleClass();
          }
          }

          這是一段平凡得不能再平凡的Java代碼,稍微有點編程語言入門知識的人都能理解它表達的意思:

          1. 創建一個名為SimpleClass的類;

          2. 定義一個入口main方法;

          3. 在main方法中創建一個SimpleClass類實例;

          4. 退出。

          什么是Java bytecode

          那么這一段代碼是怎么在機器(JVM)里運行的呢?在向下介紹之前先說清幾個概念。

          首先,Java語言和JVM完全可以看成2個完全不相干的體系。雖然JVM全稱叫Java Virtual Machine,最開始也是為了能夠實現Java的設計思想而制定開發的。但是時至今日他完全獨立于Java語言成為一套生命力更為強悍的體系工具。他有整套規范,根據這個規范它有上百個應用實現,其中包括我們最熟悉的hotspot、jrockit等。還有一些知名的變種版本——harmony和android dalvik,嚴格意義上變種版本并不能叫java虛擬機,因為其并未按照jvm規范開發,但是從設計思想、API上看又有大量的相似之處。

          其次,JVM并不能理解Java語言,他所理解的是稱之為Java bytecode的"語言"。Java bytecode從形式上來說是面向過程的,目前包含130多個指令,他更像可以直接用于CPU計算的一組指令集。所以無論什么語言,最后只要按照規范編譯成java bytecode(以下簡稱為"字節碼")都可以在JVM上運行。這也是scala、groovy、kotlin等各具特色的語言雖然在語法規則上不一致,但是最終都可以在JVM上平穩運行的原因。

          Java bytecode的規范和存儲形式

          前面代碼保存成 .java 文件然后用下面的命令編譯過后就可以生成.class字節碼了:

          $ javac SimpleClass.java #SimpleClass.class

          字節碼是直接使用2進制的方式存儲的,每一段數據都定義了具體的作用。下面是SimpleClass.class 的16進制數據(使用vim + xxd打開):

          Java

          一個 .class 文件的字節碼分為10個部分:

          0~4字節:文件頭,用于表示這是一個Java bytecode文件,值固定為0xCAFEBABE。

          2+2字節:編譯器的版本信息。

          2+n字節:常量池信息。

          2字節:入口權限標記。

          2字節:類符號名稱。

          2字節:父類符號名稱。

          2+n字節:接口。

          2+n字節:域(成員變量)。

          2+n字節:方法。

          2+n字節:屬性。

          每個部分的前2個字節都是該部分的標識位。

          本篇的目的是說明字節碼的作用以及JVM如何使用字節碼運轉的,想要詳細了解2進制意義的請看這里:http://www.jianshu.com/p/252f381a6bc4。

          反匯編及字節碼解析

          我們可以使用 javap 命令將字節碼反匯編成我們容易閱讀的格式化了的指令集編碼:

          $ javap -p SimpleClass.class #查看類和成員$ javap -s SimpleClass.class #查看方法簽名$ javap -c SimpleClass.class #反匯編字節碼$ javap -v SimpleClass.class #返匯編查看所有信息

          javap 還有很多的參數,可以使用 javap --help 來了解。下面是使用javap -v 命令輸出的內容,輸出了常量池信息、方法簽名、方法描述、堆棧數量、本地內存等信息:

          public class example.classLifecicle.SimpleClass
           flags: ACC_PUBLIC, ACC_SUPER
          Constant pool: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V
           #2 = Class #14 // example/classLifecicle/SimpleClass
           #3 = Methodref #2.#13 // example/classLifecicle/SimpleClass."<init>":()V
           #4 = Class #15 // java/lang/Object
           #5 = Utf8 <init>
           #6 = Utf8 ()V
           #7 = Utf8 Code
           #8 = Utf8 LineNumberTable
           #9 = Utf8 main
           #10 = Utf8 ([Ljava/lang/String;)V
           #11 = Utf8 SourceFile
           #12 = Utf8 SimpleClass.java
           #13 = NameAndType #5:#6 // "<init>":()V
           #14 = Utf8 example/classLifecicle/SimpleClass
           #15 = Utf8 java/lang/Object{
           public example.classLifecicle.SimpleClass();
           descriptor: ()V
           flags: ACC_PUBLIC
           Code:
           stack=1, locals=1, args_size=1
           0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V
           4: return
           LineNumberTable:
           line 3: 0
           public static void main(java.lang.String[]);
           descriptor: ([Ljava/lang/String;)V
           flags: ACC_PUBLIC, ACC_STATIC
           Code:
           stack=2, locals=2, args_size=1
           0: new #2 // class example/classLifecicle/SimpleClass
           3: dup 4: invokespecial #3 // Method "<init>":()V
           7: astore_1 8: return
           LineNumberTable:
           line 5: 0
           line 6: 8}

          下面是關于字節碼格式的描述:

          public class example.classLifecicle.SimpleClass

          這一段表示這個類的符號。

          flags: ACC_PUBLIC, ACC_SUPER

          該類的標記。例如是否是public類等等,實際上就是將一些Java關鍵字轉譯成對應的Java bytecode。

          Constant pool:

          constant pool: 之后的內容一直到 { 符號,都是我們所說的"常量池"。在對java類進行編譯之后就會產生這個常量池。通常我們所說的類加載,就是加載器將字節碼描述的常量信息轉換成實際存儲在運行時常量池中的一些內存數據(當然每個方法中的指令集也會隨之加載到方法指向的某個內存空間中)。

          "#1"可以理解為常量的ID。可以把常量池看作一個Table,每一個ID都指向一個常量,而在使用時都直接用"#1"這樣的ID來引用常量。

          常量池中的包含了運行這個類中方法所有需要用到的所有常量信息,Methodref、Class、Utf8、NameAndType等表示常量的類型,后面跟隨的參數表示這個常量的引用位置或者數值。

          {}:

          常量池之后的{}之間是方法。每一個方法分為符號(名稱)、標記、描述以及指令集。descriptor:描述。flags:入口權限標記。Code:指令集。

          Code中,stack表示這一段指令集堆棧的最大深度, locals表示本地存儲的最大個數, args_size表述傳入參數的個數。

          字節碼如何驅動機器運行

          在往下說之前,先說下JVM方法區的內容。方法區顧名思義就是存儲各種方法的地方。但是從實際應用來看,以Hotspot為例——方法區在實現時通常分為class常量池、運行常量池。在大部分書籍中,運行時常量池被描述為包括類、方法的所有描述信息以及常量數據(詳情請看這里的介紹)。

          對于機器來說并不存在什么類的感念的。到了硬件層面,他所能了解的內容就是:1)我要計算什么(cpu),2)我要存儲什么(緩存、主存、磁盤等,我們統稱內存)?

          按照分層模型來說JVM只是一個應用進程,是不可能直接和機器打交道的(這話也不是絕對的,有些虛擬機還真直接當作操作系統在特有硬件設備上用)。在JVM到硬件之間還隔著一層操作系統,在本地運行時是直接調用操作系統接口的(windows和linux都是C/C++)。不過為了JVM虛擬機更高效,字節碼設計為更接近機器邏輯行為的方式來運行。不然也沒必要弄一個字節碼來轉譯Java語言,像nodejs用的V8引擎那樣實時編譯Javascript不是更直接?這也是過去C/C++唾棄Java效率低下,到了如今Java反而去吐槽其他解釋型編譯環境跑得慢的原因(不過這也不見得100%正確。比如某些情況下Java在JVM上處理JSON不見得比JavaScript在nodejs上快,而且寫起代碼來也挺費勁的)。

          我們回到硬件計算和存儲的問題。CPU的計算過程實質上就是操作系統的線程不斷給CPU傳遞指令集。線程就像傳送帶一樣,把一系列指令排好隊然后一個一個交給CPU去處理。每一個指令告訴CPU干一件事,而干事的前后總得有個依據(輸入)和結果(輸出),這就是各種緩存、內存、磁盤的作用——提供依據、保存結果。JVM線程和操作系統線程是映射關系(mapping),而JVM的堆(heap)和非堆(Non-heap)就是一個內存管理的模型。所以我們跳出分層的概念,將字節碼理解為直接在驅動cpu和內存運行的匯編碼更容易理解。

          最后,我們回到方法區(Method Area)這個規范概念。CPU只關心一堆指令,而JVM中所有的指令都是放置在方法區中的。JVM的首要任務是把這些指令有序的組織起來,按照編程好的邏輯將指令一個一個交給CPU去運行。而CPU都是靠線程來組織指令運算的,所以JVM中每個線程都有一個線程棧,通過他將指令組織起來一個一個的交給CPU去運算——這就是計數器(Counter Register,用以指示當前應該執行什么字節碼指令)、線程棧(Stacks,線程的運算模型——先進后出) 和 棧幀(Stacks Frame,方法執行的本地變量) 的概念。所以無論多復雜的設計,方法區可以簡單的理解為:有序的將指令集組織起來,并在使用的時候可以通過某些方法找到對應的指令集合

          解析常量池

          Java

          先看 SimpleClass 字節碼中常量池中的一些數據,上圖中每一個方框表示一個常量。方框中第一行的 #1 表示當前常量的ID,第二行 Methodref 表示這個這個常量的類型,第三行 #4,#13 表示常量的值。

          我們從 #1 開始跟著每個常量的值向下延伸可以展開一根以 Utf8 類型作為葉節點的樹,每一個葉節點都是一個值。所有的方法我們都可以通過樹的方式展開得到下面的查詢字段:

          class = java/lang/Object //屬于哪個類method = "<init>" //方法名稱params = NaN //參數return = V //返回類型

          所有的方法都會以 package.class.name:(params)return 的形式存儲在方法區中,通過上面的參數很快可以定位到方法,例如 java.lang.Object."<init>":()V,這里"<init>"是構造方法專用的名稱。

          解析方法中的指令集

          方法除了用于定位的標識符外就是指令集,下面解析main方法的指令集:

          0: new #2 // class example/classLifecicle/SimpleClass3: dup4: invokespecial #3 // Method "<init>":()V7: astore_18: return

          1))new 表示新建一個ID為#2的對象即SimpleClass(#2->#15="example/classLifecicle/SimpleClass")。此時JVM會在堆上創建一個能放置SimpleClass類的空間并將引用地址返回寫到棧頂。這里僅僅完成在堆中分配空間,沒執行初始化。

          2)dup表示復制棧頂數據。此時棧中有2個指向同一內存區域的SimpleClass引用。

          3)invokespecial #3表示執行#3的方法。通過解析常量池#3就是SimpleClass的構造方法。此后會將SimpleClass構造方法中的指令壓入棧中執行。

          4)接下來來是SimpleClass的構造方法部分: a)aload_0 表示將本地內存的第一個數據壓入棧頂,本地內存的第一個數據就是this。b)invokespecial #1 表示執行 Object 的構造方法。c)退出方法。這樣就完成了實例的構造過程。

          5)完成上述步驟后,線程棧上還剩下一個指向SimpleClass實例的引用,astore_1 表示將引用存入本地緩存第二個位置。

          6)return -> 退出 main 方法。

          方法區結構

          那么在方法區中所有的類是如何組織存放的呢?

          我們用一個關系型數據庫常的結構就可以解釋他。在數據庫中我們常用的對象有3個——表、字段、數據。每一個類對應的字節碼我們都可以看成會生成2張數據庫表——常量池表、方法表。通過字節碼的解析,在內存中產生了如下結構的表:

          常量池表:example.classLifecicle.SimpleClass_Constant

          idtypevalue
          #1Methodref#4,#13
          …………
          #4Class#15
          #15Utf8java/lang/Object

          方法表:example.classLifecicle.SimpleClass_Method

          nameparamsreturnflagcode
          <init> NaNVstatic,public……
          ……………………

          然后在運行過程中當計數器遇到 invokespecial #3 這樣的指令時就會根據指令后面的ID去本類的常量表中查詢并組裝數據。當組裝出 class = java/lang/Object、method = "<init>"、params = NaN、return = V這樣的數據后,就會去名為java.lang.Object的表中根據 method、params、return 字段的數據查詢對應的code,找到后為該code創建一個本地內存,隨后線程計數器逐個執行code中的指令。

          Java

          這里僅僅用關系型數據庫表的概念來解釋方法區中如何將指令執行和字節碼對應起來,真正的JVM運行方式比這復雜得多。不過這樣很容易理解方法區到底是怎么一回事。

          者 | 王一鵬

          如果說,在以音視頻為載體傳輸信息、進行交互的技術領域,始終飄著一朵“烏云”,那么這朵“烏云”的名字,很可能既不是低延時,也不是高可靠,而是不斷變化的應用場景。

          從 Web 2.0 到移動端基礎設施全面建成,我們完成了文字信息的全面數字化;而從 2016 “直播元年”至今,圖像、語音信息的全面數字化則仍在推進中。最簡單的例證是,對于早期的流媒體直播而言,1080P 是完全可接受的高清直播;但對于今天的流媒體而言,在冬奧會這樣的直播場景下,8k 可能是個剛性需求,相比于 1080P,像素數量增長 16 倍。

          而且,今天的流媒體業務,對視頻流的要求不僅停留在分辨率上,也表現在幀率上。以阿里文娛 2019 年底推出的“幀享”解決方案為例,它將畫面幀率推至 120 FPS,同時對動態渲染的要求也很高。過往人們總說,幀率超過 24 FPS,人眼就無法識別,因此高幀率沒有實際意義。但高幀率是否能提升觀看效果,與每幀信息量密切相關,近幾年游戲開發技術的進步,以及以李安為代表的一眾電影導演,已經徹底打破這一誤解。

          對于 RTC 來說,問題情境和對應的軟件架構又截然不同。早期大家看賽事直播,20s 的延遲完全可以接受。但在 RTC 場景下,人與人的即時互動讓使用者對延遲的忍耐度急劇降低,從 WebRTC 方案到自研傳輸協議,相關嘗試從未停止。

          當我們以為,所謂的場景問題,終于可以被抽象為有限的幾個技術問題,并將延遲壓入 100ms 以內,可靠性提升至 99.99%,新的場景又出現了。全景直播、VR 全球直播,云游戲……其中又以云游戲最為典型——云游戲簡直是過去那些音視頻場景性能要求的集大成者:有的游戲要求延時低至 50ms 以內;有的要求 FPS 60 以上;分辨率不消說,肯定是越高越好。同時云游戲場景夾雜著大量的動態渲染任務,無一不在消耗著服務器資源,增大著全鏈路的傳輸延時。

          那么,如果從云游戲場景的性能要求出發,進而擴展至整個超視頻時代的架構體系,該以怎樣的思路來進行架構設計呢?只關注軟件,可能不太行的通;硬件成為必須納入考慮的一環。

          以軟件為中心并非最佳選擇

          要解釋這個問題,必須重新回顧下常規的云游戲技術架構。下圖主要參考自英特爾音視頻白皮書、華為云游戲白皮書,并做了相應調整,基本與當前環境下,大部分云游戲架構的設計相符。

          InfoQ 版權所有

          在這一架構內,至云游戲終端前,所有服務都在云端、公共網絡上完成,保證用戶無需下載游戲或是為了玩游戲購置高性能終端。游戲玩家的終端,主要負責對網絡包進行處理、對渲染后的游戲畫面進行解碼、顯示,并相應地輸入指令,回傳給服務器。

          而在服務器端,鏈路相對復雜。云游戲管理平臺是服務的起點,上下兩條鏈路,都是云游戲的周邊技術服務,與業務場景強相關,包括云游戲的直播錄制、游戲日志 / 記錄存儲等。前者對時延忍耐度較高,可以走正常的流媒體服務體系,使用 CDN 分發音視頻內容;后者屬于正常的游戲服務器設計范疇,正常提供服務即可。

          關鍵在于中間一層,也就是云游戲容器集群。這一部分要實現的設計基礎目標是保障 1s 至少完成 24 張游戲畫面(24 幀)的計算、動態渲染和編碼傳輸,部分高要求場景需要幀率達到 60 FPS,同時保證時延盡可能得低。

          這部分的技術挑戰非常大,以至于若僅以軟件為中心思考,很難做出真正突破。從相關指標的演進歷史來看,僅僅在 4 年前,移動端游戲本地渲染的基礎目標還是 30 FPS,如今雖然能實現 60 FPS 甚至更高,但討論的場景也從本地渲染切換成了云端渲染。在軟件上,除非出現學術層面的突破,否則很難保證性能始終保持這樣跨度的飛躍。

          此外,渲染本來就是嚴重倚仗硬件的工作,渲染速度和質量的提升,主要依賴于 GPU 工藝、性能以及配套軟件的提升。

          3D 游戲渲染畫面

          而更為復雜的游戲性能以及整體時延的控制,則對整個處理、傳輸鏈路提出了要求。僅以時延為例,它要求在編碼、計算、渲染、傳輸等任何一個環節的處理時間都控制在較低范圍內。同樣是在 3 - 4 年前,有業界專家分享,他們對 RPG 類云游戲的傳輸時延容忍度是 1000 ms,但事實證明,玩家并不能忍受長達 1s 的輸入延遲。反觀今日,無論是通過公有云 + GA 方案,還是通過自建實時傳輸網絡方案,即便是傳輸普通音視頻流的 RTC 服務也只能保證延時 100ms 以內,而云游戲的計算量和帶寬需求數倍于普通音視頻服務。

          以上僅僅是冰山一角。對于架構設計而言,除了高性能、高可用、可擴展性三類設計目標外,成本也是必須要考慮的平衡點——需要 1000 臺服務器的架構,和需要 100 臺服務器的架構,壓根不是一個概念。2010 年前后,云游戲基本不存在 C 端商業化可能,雖然整體時延和性能指標可以滿足當時的要求,但代價是一臺服務器只能服務一個玩家,單個玩家服務成本上萬。云游戲“元老” Onlive 公司的失敗,在當時非常能說明問題。

          而到了 2020 年,行業硬件的整體性能提升后,一臺服務器可支持 20 - 50 路并發,性能提升了幾十倍。

          那么,如果我們將硬件變成架構設計的核心考慮要素,會是什么樣的呢?大致如下圖所示(為了不讓圖示過于復雜,我們只保留了云游戲核心服務鏈路,以作代表)。

          InfoQ 版權所有

          可以看到,僅在云服務器部分,就有大量的硬件和配套軟件需要參與進來,要關注的性能點也相對復雜。而這僅僅是云游戲一個應用場景下的音視頻架構,當我們將場景抽象并擴展,最終覆蓋到整個超視頻時代的時候,以下這張來自英特爾技術團隊的架構圖,可能更加符合實際。英特爾將音視頻體系架構在軟件和硬件層面分別進行了展示:一部分叫做 Infrastructure(基礎設施層),如圖一所示;另一部分則稱其為 Infrastructure Readiness (基礎設施就緒),指的是基礎設施就緒后,建立在其上的工作負載,如圖二所示。兩張圖的首尾有一定重合,表示其頭尾相接。

          圖一:基礎設施層

          圖二:基礎設施就緒后的工作負載

          可以看到,基礎設施層主要包括硬件、配套云服務、云原生中間件以及各類開源基礎軟件。而在工作負載層面,是大量的軟件工作,包括核心的框架、SDK 以及開源軟件貢獻(UpStream)。這也是為什么英特爾以硬件聞名,卻維持著超過一萬人的軟件研發團隊。

          拆解軟硬一體的音視頻架構方案

          基礎設施層

          在基礎設施層,我們的首要關注對象就是硬件,尤其是對于音視頻服務來說,硬件提升對業務帶來的增益相當直接。

          但相比于十年前,當前的硬件產品家族的復雜度和豐富度都直線上升,其核心原因無外乎多變的場景帶來了新的計算需求,靠 CPU 吃遍天下的日子已經一去不復返了。以前面展示的英特爾硬件矩陣為例,在音視頻場景下,我們主要關注 CPU、GPU、IPU,受限于文章篇幅,網卡一類的其他硬件不在重點討論范圍內。

          在 CPU 方面,英特爾已更新至強? 第三代可擴展處理器,相比第二代內存帶寬提升 1.60 倍,內存容量提升 2.66 倍,采用 PCIe Gen 4,PCI Express 通道數量至多增加 1.33 倍。其中,英特爾? 至強? Platinum 8380 處理器可以達到 8 通道、 40 個內核,主頻 2.30 GHz,英特爾支持冬奧會轉播 8k 轉播時,CPU 側的主要方案即是 Platinum 8380。這里貼一張詳細參數列表供你參考(https://www.intel.cn/content/www/cn/zh/products/sku/212287/intel-xeon-platinum-8380-processor-60m-cache-2-30-ghz/specifications.html):

          英特爾 CPU 另外一個值得關注的特點,在于其配套軟件層面,主要是 AVX-512 指令集。AVX-512 指令集發布于 2013 年,屬于擴展指令集。老的指令集只支持一條指令操作一個數據,但隨著場景需求的變化,單指令多數據操作成為必選項,AVX 系列逐漸成為主流。目前,AVX-512 指令集的主要使用意義在于使程序可同時執行 32 次雙精度、64 次單精度浮點運算,或操作八個 64 位和十六個 32 位整數。理論上可以使浮點性能翻倍,整數計算性能增加約 33%,且目前只在 Skylake、 Ice Lake 等三代 CPU 上提供支持,因此也較為獨特。

          在視頻編解碼、 轉碼等流程中,因為應用程序需要執行大規模的整型和浮點計算,所以對 AVX-512 指令集的使用也相當關鍵。

          而 GPU 方案在云游戲場景中,通常更加引人矚目,英特爾? 服務器 GPU 是基于英特爾 Xe 架構的數據中心的第一款獨立顯卡處理單元。英特爾? 服務器 GPU 基于 23W 獨立片上系統(SoC)設計,有 96 個獨立執行單元、128 位寬流水線、8G 低功耗內存。

          所謂片上系統 SoC,英文全稱是 System on Chip,也就是系統級芯片,SoC 包括但不僅限于 CPU、GPU。就在今年,前 Mac 系統架構團隊負責人、蘋果 M1 芯片的“功臣” Jeff Wilcox 宣布離開蘋果,擔任英特爾院士(Intel Fellow)、設計工程事業群(Design Engineering Group)CTO,并負責客戶端 SoC 架構設計,也在行業內引起了眾多關注。

          當然,只有 GPU 硬件本身是不夠的,英特爾? Media SDK 幾乎是搭配 GPU 的必選項。英特爾? Media SDK 提供的是高性能軟件開發工具、庫和基礎設施,以便基于英特爾? 架構的硬件基礎設施上創建、開發、調試、測試和部署企業級媒體解決方案。

          其構成可參考下圖:

          IPU 是為了分擔 CPU 工作負載而誕生的專用芯片,2021 年 6 月,英特爾數據平臺事業部首席技術官 Guido Appenzeller 表示:“IPU 是一種全新的技術類別,是英特爾云戰略的重要支柱之一。它擴展了我們的智能網卡功能,旨在應對當下復雜的數據中心,并提升效率。”

          具體落地在音視頻場景里,IPU 要負責處理編碼后的音視頻流的傳輸,從而解放 CPU 去更多關注業務邏輯。所以,CPU + GPU + IPU 的組合,不僅是在關注不同場景下的需求滿足問題,實際上也在關注架構成本問題。

          工作負載層

          從基礎設施過渡到工作負載,實際上有一張架構圖,更詳細的展示了相關技術棧的構成:

          在這張架構圖中,橫向是從源碼流輸入到分發的整個流程,期間包含了編碼、分析等處理動作;而縱向則展示了要服務于這條音視頻處理流程,需要搭配的硬件和軟件體系。

          OneAPI 作為異構算力編程模型,是橋接基礎設施和上層負載的關鍵一層,這不必多言。而到了負載層,軟件則分成了藍色和紫色兩個色塊。藍色代表直接開源軟件,紫色則代表經過英特爾深度優化,再回饋(Upstream)給開源社區的開源軟件。

          在藍色部分,OpenVino 是個很有意思的工具套件,它圍繞深度學習推理做了大量的性能優化,并且可以兼容 TensorFlow、Caffe、MXNet 和 Kaldi 等深度學習模型訓練框架。當音視頻體系需要加入 AI 技術棧以服務超分辨率等關鍵需求時,OpenVino 會起到關鍵作用。

          紫色部分的 x.264/x.265 是一個典型。作為音視頻行業最主流的編碼標準,英特爾使其開源的主要貢獻者,而且 AVX-512 指令集也專門圍繞 x.264/x.265 做了優化和性能測試。

          另一個值得關注的核心是編碼器,它橫跨了藍色區域和紫色區域,既有行業通用的 ffmpeg,也有英特爾自研的 SVT,二者同樣引人關注。

          關于編解碼器的選型思考

          在流媒體時代,著名開源多媒體框架 ffmpeg 是業界在做編解碼處理時,絕對的參考對象。說白了,很多編解碼器就是 ffmpeg 的深度定制版本。到了 RTC 時代,出于更加嚴苛的及時交互需求,自研編解碼器盡管難度頗高,但也在研發能力過硬的企業中形成了不小的趨勢。

          可歸根結底,在推進以上工作時,軟件始終是思考的出發點,從業者們多少有些忽略對硬件的適配。

          SVT 的全稱是 Scalable Video Technology ,是開源項目 Open Visual Cloud 的重要組成部分,針對英特爾多個 CPU 進行了高度優化,因此在英特爾硬件體系上,性能表現非常突出。SVT 設計最樸素的初衷,是針對現代 CPU 的多個核進行利用率方面的提升,比如依仗硬件上的多核設計并行對多個幀同時處理,或對一張圖像分塊進而并行處理,大大加快處理速度,避免多核 CPU 空轉。

          更為人所熟知的可能是后來這個叫做 SVT-AV1 的開源項目(GitHub 地址:https://github.com/AOMediaCodec/SVT-AV1),AV1 開源視頻編碼,由英特爾、谷歌、亞馬遜、思科、蘋果、微軟等共同研發,目的是提供相比 H.265 更高效的壓縮率,降低數據存儲和網絡傳輸的成本。

          而就在今年上半年,英特爾發布了其用于 CPU 的開源編解碼器 SVT-AV1 的 1.0 版,相比 0.8 版本,性能上有著巨大提升。

          結束語

          歸根結底,盡管“摩爾定律”還在繼續,但當下已過了靠吃“硬件紅利”就能搞定新應用場景的“甜蜜期”。

          今天,我們需要了解的是以 CPU 、GPU、加速器和 FPGA 等硬件為核心的復合架構,也被稱之為由標量、矢量、矩陣、空間組成的 SVMS 架構。這一概念由英特爾率先提出,并迅速成為業內最主要的硬件架構策略。

          位于硬件之上的開發者工具也存在同樣的趨勢,英特爾的 oneAPI 就是一個典型作品。只是對于開發者工具來說,目前最主要的工作不是性能提升,而是生態和整合。

          從硬件到基礎軟件,再到開發者工具,整個基礎設施層呈現高度復雜化的架構演進趨勢,既是對架構師工作的嚴峻挑戰,也給了所有架構師更大的發揮空間。對于架構師來說,如何為自己的企業算清楚成本,在追求高性能、高可用的同時,將硬件一并納入考慮并高度重視,才是重中之重。


          主站蜘蛛池模板: 国产精品视频第一区二区三区| 亚洲一区二区影院| 国产成人高清精品一区二区三区 | 成人精品视频一区二区三区尤物 | 国产精品亚洲一区二区三区在线| 国产精品主播一区二区| 精品一区二区三区四区电影| 国产午夜精品片一区二区三区| 国产精品无码一区二区三区在| 亚洲av无码一区二区三区乱子伦| 另类国产精品一区二区| 日韩一区二区三区视频| 国产伦精品一区二区| 一区二区三区福利视频| 国产亚洲一区区二区在线| 日本中文一区二区三区亚洲| 无码8090精品久久一区| 福利一区国产原创多挂探花| 国产精久久一区二区三区| 亚洲高清成人一区二区三区| 国产一区二区高清在线播放| 国产一区二区免费视频| 中文字幕精品一区二区日本| 国产香蕉一区二区精品视频| 天海翼一区二区三区高清视频| 国产一区二区在线视频播放| 国产福利一区二区在线视频| 精品福利一区二区三| 亚洲一区二区三区在线观看网站| 无码国产精品一区二区高潮| 久久精品一区二区影院 | 日本无码一区二区三区白峰美 | 国产不卡视频一区二区三区 | 国产乱码精品一区二区三区中文| 免费无码VA一区二区三区| 四虎成人精品一区二区免费网站| 国产福利电影一区二区三区,免费久久久久久久精 | 色一情一乱一伦一区二区三区日本 | 精品视频在线观看一区二区| 国产成人一区二区三区在线观看| 亚洲AV一区二区三区四区|