整合營銷服務商

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

          免費咨詢熱線:

          聊聊 10 種系統性能優化手段

          聊聊 10 種系統性能優化手段

          天在網上沖浪,悄悄地卷你們的時候看到一個關于性能優化得不錯的文章。作者寫了上中下三篇,由淺入深地寫了關于性能優化的方方面面,并不僅僅局限于代碼層面。

          我看了之后還是很有收獲的,同時也驚嘆于作者扎實的技術能力與思考能力。于是借花獻佛,把作者的三篇整理合并之后分享給大家。希望你也能有所收獲。

          原文鏈接:https://code2life.top/2020/08/15/0055-performance/

          整理:why 技術

          上篇

          引言:取與舍

          軟件設計開發某種意義上是“取”與“舍”的藝術。

          關于性能方面,就像建筑設計成抗震 9 度需要額外的成本一樣,高性能軟件系統也意味著更高的實現成本,有時候與其他質量屬性甚至會沖突,比如安全性、可擴展性、可觀測性等等。

          大部分時候我們需要的是:在業務遇到瓶頸之前,利用常見的技術手段將系統優化到預期水平。

          那么,性能優化有哪些技術方向和手段呢?

          性能優化通常是“時間”與“空間”的互換與取舍。

          本篇分兩個部分,在上篇,講解六種通用的“時間”與“空間”互換取舍的手段:

          • 索引術
          • 壓縮術
          • 緩存術
          • 預取術
          • 削峰填谷術
          • 批量處理術

          在下篇,介紹四種進階性的內容,大多與提升并行能力有關

          • 八門遁甲 —— 榨干計算資源
          • 影分身術 —— 水平擴容
          • 奧義 —— 分片術
          • 秘術 —— 無鎖術

          每種性能優化的技術手段,我都找了一張應景的《火影忍者》中人物或忍術的配圖,評論區答出任意人物或忍術送一顆小星星。

          (注:所有配圖來自動漫《火影忍者》,部分圖片添加了文字方便理解,僅作技術交流用途)

          索引術

          10ms 之后。

          索引的原理是拿額外的存儲空間換取查詢時間,增加了寫入數據的開銷,但使讀取數據的時間復雜度一般從 O(n)降低到 O(logn)甚至 O(1)。

          索引不僅在數據庫中廣泛使用,前后端的開發中也在不知不覺運用。

          在數據集比較大時,不用索引就像從一本沒有目錄而且內容亂序的新華字典查一個字,得一頁一頁全翻一遍才能找到;

          用索引之后,就像用拼音先在目錄中先找到要查到字在哪一頁,直接翻過去就行了。

          書籍的目錄是典型的樹狀結構,那么軟件世界常見的索引有哪些數據結構,分別在什么場景使用呢?

          • 哈希表(Hash Table):哈希表的原理可以類比銀行辦業務取號,給每個人一個號(計算出的 Hash 值),叫某個號直接對應了某個人,索引效率是最高的 O(1),消耗的存儲空間也相對更大。K-V 存儲組件以及各種編程語言提供的 Map/Dict 等數據結構,多數底層實現是用的哈希表。
          • 二叉搜索樹(Binary Search Tree):有序存儲的二叉樹結構,在編程語言中廣泛使用的紅黑樹屬于二叉搜索樹,確切的說是“不完全平衡的”二叉搜索樹。從 C++、Java 的 TreeSet、TreeMap,到 Linux 的 CPU 調度,都能看到紅黑樹的影子。Java 的 HashMap 在發現某個 Hash 槽的鏈表長度大于 8 時也會將鏈表升級為紅黑樹,而相比于紅黑樹“更加平衡”的 AVL 樹反而實際用得更少。
          • 平衡多路搜索樹(B-Tree):這里的 B 指的是 Balance 而不是 Binary,二叉樹在大量數據場景會導致查找深度很深,解決辦法就是變成多叉樹,MongoDB 的索引用的就是 B-Tree。
          • 葉節點相連的平衡多路搜索樹(B+ Tree):B+ Tree 是 B-Tree 的變體,只有葉子節點存數據,葉子與相鄰葉子相連,MySQL 的索引用的就是 B+樹,Linux 的一些文件系統也使用的 B+樹索引 inode。其實 B+樹還有一種在枝椏上再加鏈表的變體:B*樹,暫時沒想到實際應用。
          • 日志結構合并樹(LSM Tree):Log Structured Merge Tree,簡單理解就是像日志一樣順序寫下去,多層多塊的結構,上層寫滿壓縮合并到下層。LSM Tree 其實本身是為了優化寫性能犧牲讀性能的數據結構,并不能算是索引,但在大數據存儲和一些 NoSQL 數據庫中用得很廣泛,因此這里也列進去了。
          • 字典樹(Trie Tree):又叫前綴樹,從樹根串到樹葉就是數據本身,因此樹根到枝椏就是前綴,枝椏下面的所有數據都是匹配該前綴的。這種結構能非常方便的做前綴查找或詞頻統計,典型的應用有:自動補全、URL 路由。其變體基數樹(Radix Tree)在 Nginx 的 Geo 模塊處理子網掩碼前綴用了;Redis 的 Stream、Cluster 等功能的實現也用到了基數樹(Redis 中叫 Rax)。
          • 跳表(Skip List):是一種多層結構的有序鏈表,插入一個值時有一定概率“晉升”到上層形成間接的索引。跳表更適合大量并發寫的場景,不存在紅黑樹的再平衡問題,Redis 強大的 ZSet 底層數據結構就是哈希加跳表。
          • 倒排索引(Inverted index):這樣翻譯不太直觀,可以叫“關鍵詞索引”,比如書籍末頁列出的術語表就是倒排索引,標識出了每個術語出現在哪些頁,這樣我們要查某個術語在哪用的,從術語表一查,翻到所在的頁數即可。倒排索引在全文索引存儲中經常用到,比如 ElasticSearch 非常核心的機制就是倒排索引;Prometheus 的時序數據庫按標簽查詢也是在用倒排索引。

          數據庫主鍵之爭:自增長 vs UUID。主鍵是很多數據庫非常重要的索引,尤其是 MySQL 這樣的 RDBMS 會經常面臨這個難題:是用自增長的 ID 還是隨機的 UUID 做主鍵?

          自增長 ID 的性能最高,但不好做分庫分表后的全局唯一 ID,自增長的規律可能泄露業務信息;而 UUID 不具有可讀性且太占存儲空間。

          爭執的結果就是找一個兼具二者的優點的折衷方案:

          用雪花算法生成分布式環境全局唯一的 ID 作為業務表主鍵,性能尚可、不那么占存儲、又能保證全局單調遞增,但引入了額外的復雜性,再次體現了取舍之道。

          再回到數據庫中的索引,建索引要注意哪些點呢?

          • 定義好主鍵并盡量使用主鍵,多數數據庫中,主鍵是效率最高的聚簇索引;
          • 在 Where 或 Group By、Order By、Join On 條件中用到的字段也要按需建索引或聯合索引,MySQL 中搭配 explain 命令可以查詢 DML 是否利用了索引;
          • 類似枚舉值這樣重復度太高的字段不適合建索引(如果有位圖索引可以建),頻繁更新的列不太適合建索引;
          • 單列索引可以根據實際查詢的字段升級為聯合索引,通過部分冗余達到索引覆蓋,以避免回表的開銷;
          • 盡量減少索引冗余,比如建 A、B、C 三個字段的聯合索引,Where 條件查詢 A、A and B、A and B and C
          • 都可以利用該聯合索引,就無需再給 A 單獨建索引了;根據數據庫特有的索引特性選擇適合的方案,比如像 MongoDB,還可以建自動刪除數據的 TTL 索引、不索引空值的稀疏索引、地理位置信息的 Geo 索引等等。

          數據庫之外,在代碼中也能應用索引的思維,比如對于集合中大量數據的查找,使用 Set、Map、Tree 這樣的數據結構,其實也是在用哈希索引或樹狀索引,比直接遍歷列表或數組查找的性能高很多。

          緩存術

          緩存優化性能的原理和索引一樣,是拿額外的存儲空間換取查詢時間。緩存無處不在,設想一下我們在瀏覽器打開這篇文章,會有多少層緩存呢?

          • 首先解析 DNS 時,瀏覽器一層 DNS 緩存、操作系統一層 DNS 緩存、DNS 服務器鏈上層層緩存;
          • 發送一個 GET 請求這篇文章,服務端很可能早已將其緩存在 KV 存儲組件中了;
          • 即使沒有擊中緩存,數據庫服務器內存中也緩存了最近查詢的數據;
          • 即使沒有擊中數據庫服務器的緩存,數據庫從索引文件中讀取,操作系統已經把熱點文件的內容放置在 Page Cache 中了;
          • 即使沒有擊中操作系統的文件緩存,直接讀取文件,大部分固態硬盤或者磁盤本身也自帶緩存;
          • 數據取到之后服務器用模板引擎渲染出 HTML,模板引擎早已解析好緩存在服務端內存中了;
          • 歷經數十毫秒之后,終于服務器返回了一個渲染后的 HTML,瀏覽器端解析 DOM 樹,發送請求來加載靜態資源;
          • 需要加載的靜態資源可能因 Cache-Control 在瀏覽器本地磁盤和內存中已經緩存了;
          • 即使本地緩存到期,也可能因 Etag 沒變服務器告訴瀏覽器 304 Not Modified 繼續緩存;
          • 即使 Etag 變了,靜態資源服務器也因其他用戶訪問過早已將文件緩存在內存中了;
          • 加載的 JS 文件會丟到 JS 引擎執行,其中可能涉及的種種緩存就不再展開了;
          • 整個過程中鏈條上涉及的所有的計算機和網絡設備,執行的熱點代碼和數據很可能會載入 CPU 的多級高速緩存。

          這里列舉的僅僅是一部分常見的緩存,就有多種多樣的形式:從廉價的磁盤到昂貴的 CPU 高速緩存,最終目的都是用來換取寶貴的時間。

          既然緩存那么好,那么問題就來了:緩存是“銀彈”嗎?

          不,Phil Karlton 曾說過:

          There are only two hard things in Computer Science: cache invalidation and naming things.

          計算機科學中只有兩件困難的事情:緩存失效和命名規范。

          緩存的使用除了帶來額外的復雜度以外,還面臨如何處理緩存失效的問題。

          • 多線程并發編程需要用各種手段(比如 Java 中的 synchronized volatile)防止并發更新數據,一部分原因就是防止線程本地緩存的不一致;
          • 緩存失效衍生的問題還有:緩存穿透、緩存擊穿、緩存雪崩。解決用不存在的 Key 來穿透攻擊,需要用空值緩存或布隆過濾器;解決單個緩存過期后,瞬間被大量惡意查詢擊穿的問題需要做查詢互斥;解決某個時間點大量緩存同時過期的雪崩問題需要添加隨機 TTL;
          • 熱點數據如果是多級緩存,在發生修改時需要清除或修改各級緩存,這些操作往往不是原子操作,又會涉及各種不一致問題。

          除了通常意義上的緩存外,對象重用的池化技術,也可以看作是一種緩存的變體。

          常見的諸如 JVM,V8 這類運行時的常量池、數據庫連接池、HTTP 連接池、線程池、Golang 的 sync.Pool 對象池等等。

          在需要某個資源時從現有的池子里直接拿一個,稍作修改或直接用于另外的用途,池化重用也是性能優化常見手段。

          壓縮術

          說完了兩個“空間換時間”的,我們再看一個“時間換空間”的辦法——壓縮。

          壓縮的原理消耗計算的時間,換一種更緊湊的編碼方式來表示數據。

          為什么要拿時間換空間?時間不是最寶貴的資源嗎?

          舉一個視頻網站的例子,如果不對視頻做任何壓縮編碼,因為帶寬有限,巨大的數據量在網絡傳輸的耗時會比編碼壓縮的耗時多得多。

          對數據的壓縮雖然消耗了時間來換取更小的空間存儲,但更小的存儲空間會在另一個維度帶來更大的時間收益。

          這個例子本質上是:“操作系統內核與網絡設備處理負擔 vs 壓縮解壓的 CPU/GPU 負擔”的權衡和取舍。

          我們在代碼中通常用的是無損壓縮,比如下面這些場景:

          • HTTP 協議中 Accept-Encoding 添加 Gzip/deflate,服務端對接受壓縮的文本(JS/CSS/HTML)請求做壓縮,大部分圖片格式本身已經是壓縮的無需壓縮;
          • HTTP2 協議的頭部 HPACK 壓縮;
          • JS/CSS 文件的混淆和壓縮(Uglify/Minify);
          • 一些 RPC 協議和消息隊列傳輸的消息中,采用二進制編碼和壓縮(Gzip、Snappy、LZ4 等等);
          • 緩存服務存過大的數據,通常也會事先壓縮一下再存,取的時候解壓;
          • 一些大文件的存儲,或者不常用的歷史數據存儲,采用更高壓縮比的算法存儲;
          • JVM 的對象指針壓縮,JVM 在 32G 以下的堆內存情況下默認開啟“UseCompressedOops”,用 4 個 byte 就可以表示一個對象的指針,這也是 JVM 盡量不要把堆內存設置到 32G 以上的原因;
          • MongoDB 的二進制存儲的 BSON 相對于純文本的 JSON 也是一種壓縮,或者說更緊湊的編碼。但更緊湊的編碼也意味著更差的可讀性,這一點也是需要取舍的。純文本的 JSON 比二進制編碼要更占存儲空間但卻是 REST API 的主流,因為數據交換的場景下的可讀性是非常重要的。

          信息論告訴我們,無損壓縮的極限是信息熵。進一步減小體積只能以損失部分信息為代價,也就是有損壓縮。

          那么,有損壓縮有哪些應用呢?

          • 預覽和縮略圖,低速網絡下視頻降幀、降清晰度,都是對信息的有損壓縮;
          • 音視頻等多媒體數據的采樣和編碼大多是有損的,比如 MP3 是利用傅里葉變換,有損地存儲音頻文件;jpeg 等圖片編碼也是有損的。雖然有像 WAV/PCM 這類無損的音頻編碼方式,但多媒體數據的采樣本身就是有損的,相當于只截取了真實世界的極小一部分數據;
          • 散列化,比如 K-V 存儲時 Key 過長,先對 Key 執行一次“傻”系列(SHA-1、SHA-256)哈希算法變成固定長度的短 Key。另外,散列化在文件和數據驗證(MD5、CRC、HMAC)場景用的也非常多,無需耗費大量算力對比完整的數據。

          除了有損/無損壓縮,但還有一個辦法,就是壓縮的極端——從根本上減少數據或徹底刪除。

          能減少的就減少:

          • JS 打包過程“搖樹”,去掉沒有使用的文件、函數、變量;
          • 開啟 HTTP/2 和高版本的 TLS,減少了 Round Trip,節省了 TCP 連接,自帶大量性能優化;
          • 減少不必要的信息,比如 Cookie 的數量,去掉不必要的 HTTP 請求頭;
          • 更新采用增量更新,比如 HTTP 的 PATCH,只傳輸變化的屬性而不是整條數據;
          • 縮短單行日志的長度、縮短 URL、在具有可讀性情況下用短的屬性名等等;
          • 使用位圖和位操作,用風騷的位操作最小化存取的數據。典型的例子有:用 Redis 的位圖來記錄統計海量用戶登錄狀態;布隆過濾器用位圖排除不可能存在的數據;大量開關型的設置的存儲等等。

          能刪除的就刪除:

          • 刪掉不用的數據;
          • 刪掉不用的索引;
          • 刪掉不該打的日志;
          • 刪掉不必要的通信代碼,不去發不必要的 HTTP、RPC 請求或調用,輪詢改發布訂閱;
          • 終極方案:砍掉整個功能。

          畢竟有位叫做 Kelsey Hightower 的大佬曾經說過:

          No code is the best way to write secure and reliable applications. Write nothing; deploy nowhere

          不寫代碼,是編寫安全可靠的應用程序的最佳方式。什么都不寫;哪里都不部署。

          預取術

          預取通常搭配緩存一起用,其原理是在緩存空間換時間基礎上更進一步,再加上一次“時間換時間”,也就是:用事先預取的耗時,換取第一次加載的時間。

          當可以猜測出以后的某個時間很有可能會用到某種數據時,把數據預先取到需要用的地方,能大幅度提升用戶體驗或服務端響應速度。

          是否用預取模式就像自助餐餐廳與廚師現做的區別,在自助餐餐廳可以直接拿做好的菜品,一般餐廳需要坐下來等菜品現做。

          那么,預取在哪些實際場景會用呢?

          • 視頻或直播類網站,在播放前先緩沖一小段時間,就是預取數據。有的在播放時不僅預取這一條數據,甚至還會預測下一個要看的其他內容,提前把數據取到本地;
          • HTTP/2 Server Push,在瀏覽器請求某個資源時,服務器順帶把其他相關的資源一起推回去,HTML/JS/CSS 幾乎同時到達瀏覽器端,相當于瀏覽器被動預取了資源;
          • 一些客戶端軟件會用常駐進程的形式,提前預取數據或執行一些代碼,這樣可以極大提高第一次使用的打開速度;
          • 服務端同樣也會用一些預熱機制,一方面熱點數據預取到內存提前形成多級緩存;另一方面也是對運行環境的預熱,載入 CPU 高速緩存、熱點函數 JIT 編譯成機器碼等等;
          • 熱點資源提前預分配到各個實例,比如:秒殺、售票的庫存性質的數據;分布式唯一 ID 等等

          天上不會掉餡餅,預取也是有副作用的

          正如烤箱預熱需要消耗時間和額外的電費,在軟件代碼中做預取/預熱的副作用通常是啟動慢一些、占用一些閑時的計算資源、可能取到的不一定是后面需要的。

          削峰填谷術

          削峰填谷的原理也是“時間換時間”,谷時換峰時。

          削峰填谷與預取是反過來的:預取是事先花時間做,削峰填谷是事后花時間做。就像三峽大壩可以抗住短期巨量洪水,事后雨停再慢慢開閘防水。軟件世界的“削峰填谷”是類似的,只是不是用三峽大壩實現,而是用消息隊列、異步化等方式。

          常見的有這幾類問題,我們分別來看每種對應的解決方案:

          • 針對前端、客戶端的啟動優化或首屏優化:代碼和數據等資源的延時加載、分批加載、后臺異步加載、或按需懶加載等等。
          • 背壓控制 - 限流、節流、去抖等等。一夫當關,萬夫莫開,從入口處削峰,防止一些惡意重復請求以及請求過于頻繁的爬蟲,甚至是一些 DDoS 攻擊。簡單做法有網關層根據單個 IP 或用戶用漏桶控制請求速率和上限;前端做按鈕的節流去抖防止重復點擊;網絡層開啟 TCP SYN Cookie 防止惡意的 SYN 洪水攻擊等等。徹底杜絕爬蟲、黑客手段的惡意洪水攻擊是很難的,DDoS 這類屬于網絡安全范疇了。
          • 針對正常的業務請求洪峰,用消息隊列暫存再異步化處理:常見的后端消息隊列 Kafka、RocketMQ 甚至 Redis 等等都可以做緩沖層,第一層業務處理直接校驗后丟到消息隊列中,在洪峰過去后慢慢消費消息隊列中的消息,執行具體的業務。另外執行過程中的耗時和耗計算資源的操作,也可以丟到消息隊列或數據庫中,等到谷時處理。
          • 捋平毛刺:有時候洪峰不一定來自外界,如果系統內部大量定時任務在同一時間執行,或與業務高峰期重合,很容易在監控中看到“毛刺”——短時間負載極高。一般解決方案就是錯峰執行定時任務,或者分配到其他非核心業務系統中,把“毛刺”攤平。比如很多數據分析型任務都放在業務低谷期去執行,大量定時任務在創建時盡量加一些隨機性來分散執行時間。
          • 避免錯誤風暴帶來的次生洪峰:有時候網絡抖動或短暫宕機,業務會出現各種異常或錯誤。這時處理不好很容易帶來次生災害,比如:很多代碼都會做錯誤重試,不加控制的大量重試甚至會導致網絡抖動恢復后的瞬間,積壓的大量請求再次沖垮整個系統;還有一些代碼沒有做超時、降級等處理,可能導致大量的等待耗盡 TCP 連接,進而導致整個系統被沖垮。解決之道就是做限定次數、間隔指數級增長的 Back-Off 重試,設定超時、降級策略。

          批量處理術

          批量處理同樣可以看成“時間換時間”,其原理是減少了重復的事情,是一種對執行流程的壓縮。以個別批量操作更長的耗時為代價,在整體上換取了更多的時間。

          批量處理的應用也非常廣泛,我們還是從前端開始講:

          • 打包合并的 JS 文件、雪碧圖等等,將一批資源集中到一起,一次性傳輸
          • 前端動畫使用 requestAnimationFrame 在 UI 渲染時批量處理積壓的變化,而不是有變化立刻更新,在游戲開發中也有類似的應用;
          • 前后端中使用隊列暫存臨時產生的數據,積壓到一定數量再批量處理;在不影響可擴展性情況下,一個接口傳輸多種需要的數據,減少大量 ajax 調用(GraphQL 在這一點就做到了極致);
          • 系統間通信盡量發送整批數據,比如消息隊列的發布訂閱、存取緩存服務的數據、RPC 調用、插入或更新數據庫等等,能批量做盡可能批量做,因為這些系統間通信的 I/O 時間開銷已經很昂貴了;
          • 數據積壓到一定程度再落盤,操作系統本身的寫文件就是這么做的,Linux 的 fwrite 只是寫入緩沖區暫存,積壓到一定程度再 fsync 刷盤。在應用層,很多高性能的數據庫和 K-V 存儲的實現都體現了這一點:一些 NoSQL 的 LSM Tree 的第一層就是在內存中先積壓到一定大小再往下層合并;Redis 的 RDB 結合 AOF 的落盤機制;Linux 系統調用也提供了批量讀寫多個緩沖區文件的系統調用:readv/writev;
          • 延遲地批量回收資源,比如 JVM 的 Survivor Space 的 S0 和 S1 區互換、Redis 的 Key 過期的清除策略。

          批量處理如此好用,那么問題來了,每一批放多大最合適呢?

          這個問題其實沒有定論,有一些個人經驗可以分享。

          • 前端把所有文件打包成單個 JS,大部分時候并不是最優解。Webpack 提供了很多分塊的機制,CSS 和 JS 分開、JS 按業務分更小的 Chunk 結合懶加載、一些體積大又不用在首屏用的第三方庫設置 external 或單獨分塊,可能整體性能更高。不一定要一批搞定所有事情,分幾個小批次反而用戶體驗的性能更好。
          • Redis 的 MGET、MSET 來批量存取數據時,每批大小不宜過大,因為 Redis 主線程只有一個,如果一批太大執行期間會讓其他命令無法響應。經驗上一批 50-100 個 Key 性能是不錯的,但最好在真實環境下用真實大小的數據量化度量一下,做 Benchmark 測試才能確定一批大小的最優值。
          • MySQL、Oracle 這類 RDBMS,最優的批量 Insert 的大小也視數據行的特性而定。我之前在 2U8G 的 Oracle 上用一些普遍的業務數據做過測試,批量插入時每批 5000-10000 條數據性能是最高的,每批過大會導致 DML 的解析耗時過長,甚至單個 SQL 語句體積超限,單批太多反而得不償失。
          • 消息隊列的發布訂閱,每批的消息長度盡量控制在 1MB 以內,有些云服務商提供的消息隊列限制了最大長度,那這個長度可能就是性能拐點,比如 AWS 的 SQS 服務對單條消息的限制是 256KB。

          總之,多大一批可以確保單批響應時間不太長的同時讓整體性能最高,是需要在實際情況下做基準測試的,不能一概而論。而批量處理的副作用在于:處理邏輯會更加復雜,尤其是一些涉及事務、并發的問題;需要用數組或隊列用來存放緩沖一批數據,消耗了額外的存儲空間。

          中篇

          引言

          前面我們總結了六種普適的性能優化方法,包括 索引、壓縮、緩存、預取、削峰填谷、批量處理,簡單講解了每種技術手段的原理和實際應用。

          在開啟最后一篇前,我們先需要搞清楚:

          在程序運行期間,時間和空間都耗在哪里了?

          時間都去哪兒了?

          人眨一次眼大約 100 毫秒,而現代 1 核 CPU 在一眨眼的功夫就可以執行數億條指令。

          現代的 CPU 已經非常厲害了,頻率已經達到了 GHz 級別,也就是每秒數十億個指令周期。

          即使一些 CPU 指令需要多個時鐘周期,但由于有流水線機制的存在,平均下來大約每個時鐘周期能執行 1 條指令,比如一個 3GHz 頻率的 CPU 核心,每秒大概可以執行 20 億到 40 億左右的指令數量。

          程序運行還需要 RAM,也可能用到持久化存儲,網絡等等。隨著新的技術和工藝的出現,這些硬件也越來越厲害,比如 CPU 高速緩存的提升、NVMe 固態硬盤相對 SATA 盤讀寫速率和延遲的飛躍等等。這些硬件具體有多強呢?

          有一個非常棒的網站“Latency Numbers Every Programmer Should Know”,可以直觀地查看從 1990 年到現在,高速緩存、內存、硬盤、網絡時間開銷的具體數值。

          https://colin-scott.github.io/personal_website/research/interactive_latency.html

          下圖是 2020 年的截圖,的確是“每個開發者應該知道的數字”。

          圖片

          這里有幾個非常關鍵的數據:

          • 存取一次 CPU 多級高速緩存的時間大約 1-10 納秒級別;
          • 存取一次主存(RAM)的時間大概在 100 納秒級別;
          • 固態硬盤的一次隨機讀寫大約在 10 微秒到 1 毫秒這個數量級;
          • 網絡包在局域網傳輸一個來回大約是 0.5 毫秒。

          看到不同硬件之間數量級的差距,就很容易理解性能優化的一些技術手段了。

          比如一次網絡傳輸的時間,是主存訪問的 5000 倍,明白這點就不難理解寫 for 循環發 HTTP 請求,為什么會被扣工資了。

          放大到我們容易感知的時間范圍,來理解 5000 倍的差距:如果一次主存訪問是 1 天的話,一趟局域網數據傳輸就要 13.7 年。

          如果要傳輸更多網絡數據,每兩個網絡幀之間還有固定的間隔(Interpacket Gap),在間隔期間傳輸 Idle 信號,數據鏈路層以此來區分兩個數據包,具體數值在鏈接 Wiki 中有,這里截取幾個我們熟悉的網絡來感受一下:

          • 百兆以太網: 0.96 μs
          • 千兆以太網:96 ns
          • 萬兆以太網:9.6 ns

          不過,單純看硬件的上限意義不大,從代碼到機器指令中間有許多層抽象,僅僅是在 TCP 連接上發一個字節的數據包,從操作系統內核到網線,涉及到的基礎設施級別的軟硬件不計其數。到了應用層,單次操作耗時雖然沒有非常精確的數字,但經驗上的范圍也值得參考:

          • 用 Memcached/Redis 存取緩存數據:1-5 ms
          • 執行一條簡單的數據庫查詢或更新操作:5-50ms
          • 在局域網中的 TCP 連接上收發一趟數據包:1-10ms;廣域網中大約 10-200ms,視傳輸距離和網絡節點的設備而定
          • 從用戶態切換到內核態,完成一次系統調用:100ns - 1 μs,視不同的系統調用函數和硬件水平而定,少數系統調用可能遠超此范圍。

          空間都去哪兒了?

          在計算機歷史上,非易失存儲技術的發展速度超過了摩爾定律。除了嵌入式設備、數據庫系統等等,現在大部分場景已經不太需要優化持久化存儲的空間占用了,這里主要講的是另一個相對稀缺的存儲形式 —— RAM,或者說主存/內存。

          以 JVM 為例,在堆里面有很多我們創建的對象(Object)。

          • 每個 Object 都有一個包含 Mark 和類型指針的 Header,占 12 個字節
          • 每個成員變量,根據數據類型的不同占不同的字節數,如果是另一個對象,其對象指針占 4 個字節
          • 數組會根據聲明的大小,占用 N 倍于其類型 Size 的字節數
          • 成員變量之間需要對齊到 4 字節,每個對象之間需要對齊到 8 字節

          如果在 32G 以上內存的機器上,禁用了對象指針壓縮,對象指針會變成 8 字節,包括 Header 中的 Klass 指針,這也就不難理解為什么堆內存超過 32G,JVM 的性能直線下降了。

          舉個例子,一個有 8 個 int 類型成員的對象,需要占用 48 個字節(12+32+4),如果有十萬個這樣的 Object,就需要占用 4.58MB 的內存了。這個數字似乎看起來不大,而實際上一個 Java 服務的堆內存里面,各種各樣的對象占用的內存通常比這個數字多得多,大部分內存耗在 char[]這類數組或集合型數據類型上。

          舉個例子,一個有 8 個 int 類型成員的對象,需要占用 48 個字節(12+32+4),如果有十萬個這樣的 Object,就需要占用 4.58MB 的內存了。這個數字似乎看起來不大,而實際上一個 Java 服務的堆內存里面,各種各樣的對象占用的內存通常比這個數字多得多,大部分內存耗在 char[]這類數組或集合型數據類型上。

          堆內存之外,又是另一個世界了。

          從操作系統進程的角度去看,也有不少耗內存的大戶,不管什么 Runtime 都逃不開這些空間開銷:每個線程需要分配 MB 級別的線程棧,運行的程序和數據會緩存下來,用到的輸入輸出設備需要緩沖區……

          代碼“寫出來”的內存占用,僅僅是冰山之上的部分,真正的內存占用比“寫出來”的要更多,到處都存在空間利用率的問題。

          比如,即使我們在 Java 代碼中只是寫了 response.getWriter().print(“OK”),給瀏覽器返回 2 字節,網絡協議棧的層層封裝,協議頭部不斷增加的額外數據,讓最終返回給瀏覽器的字節數遠超原始的 2 字節,像 IP 協議的報頭部就至少有 20 個字節,而數據鏈路層的一個以太網幀頭部至少有 18 字節。

          如果傳輸的數據過大,各層協議還有最大傳輸單元 MTU 的限制,IPv4 一個報文最大只能有 64K 比特,超過此值需要分拆發送并在接收端組合,更多額外的報頭導致空間利用率降低(IPv6 則提供了 Jumbogram 機制,最大單包 4G 比特,“浪費”就減少了)。

          這部分的“浪費”有多大呢?下面的鏈接有個表格,傳輸 1460 個字節的載荷,經過有線到無線網絡的轉換,至少再添 120 個字節,**空間利用率<92.4%**。

          https://en.wikipedia.org/wiki/Jumbo_frame

          這種現象非常普遍,使用抽象層級越高的技術平臺,平臺提供高級能力的同時,其底層實現的“信息密度”通常越低。

          像 Java 的 Object Header 就是使用 JVM 的代價,而更進一步使用動態類型語言,要為靈活性付出空間的代價則更大。哈希表的自動擴容,強大的反射能力等等,背后也付出了空間的代價。

          再比如,二進制數據交換協議通常比純文本協議更加節約空間。但多數廠家我們仍然用 JSON、XML 等純文本協議,用信息的冗余來換取可讀性。即便是二進制的數據交互格式,也會存在信息冗余,只能通過更好的協議和壓縮算法,盡量去逼近壓縮的極限 —— 信息熵。

          小結

          理解了時間和空間的消耗在哪后,還不能完全解釋軟件為何傾向于耗盡硬件資源。有一條定律可以解釋,正是它錘爆了摩爾定律。

          它就是安迪-比爾定律。

          “安迪給什么,比爾拿走什么”。

          安迪指的是 Intel 前 CEO 安迪·葛洛夫,比爾指的是比爾·蓋茨。

          這句話的意思就是:軟件發展比硬件還快,總能吃得下硬件

          20 年前,在最強的計算機也不見得可以玩賽車游戲;

          10 年前,個人電腦已經可以玩畫質還可以的 3D 賽車游戲了;

          現在,自動駕駛+5G 云駕駛已經快成為現實。

          在這背后,是無數的硬件技術飛躍,以及吃掉了這些硬件的各類軟件。

          這也是我們每隔兩三年都要換手機的原因:不是機器老化變卡了,是嗜血的軟件在作怪。

          圖片

          因此,即使現代的硬件水平已經強悍到如此境地,性能優化仍然是有必要的。

          軟件日益復雜,抽象層級越來越高,就越需要底層基礎設施被充分優化。

          對于大部分開發者而言,高層代碼逐步走向低代碼化、可視化,“一行代碼”能產生的影響也越來越大,寫出低效代碼則會吃掉更多的硬件資源。

          下篇

          引言

          本篇也是本系列最硬核的一篇,本人技術水平有限,可能存在疏漏或錯誤之處,望斧正。仍然選取了《火影忍者》的配圖和命名方式幫助理解:

          • 八門遁甲 —— 榨干計算資源
          • 影分身術 —— 水平擴容
          • 奧義 —— 分片術
          • 秘術 —— 無鎖術

          (注:這些“中二”的前綴僅是用《火影》中的一些術語,形象地描述技術方案)

          八門遁甲 —— 榨干計算資源

          讓硬件資源都在處理真正有用的邏輯計算,而不是做無關的事情或空轉。

          從晶體管到集成電路、驅動程序、操作系統、直到高級編程語言的層層抽象,每一層抽象帶來的更強的通用性、更高的開發效率,多是以損失運行效率為代價的。

          但我們可以在用高級編程語言寫代碼的時候,在保障可讀性、可維護性基礎上用運行效率更高、更適合運行時環境的方式去寫,減少額外的性能損耗《Effective XXX》、《More Effective XXX》、《高性能 XXX》這類書籍所傳遞的知識和思想。

          落到技術細節,下面用四個小節來說明如何減少“無用功”、避免空轉、榨干硬件。

          聚焦

          減少系統調用與上下文切換,讓 CPU 聚焦。

          可以看看兩個 stackoverflow 上的帖子:

          https://stackoverflow .com/questions/21887797/what-is-the-overhead-of-a-context-switch https://stackoverflow.com/questions/23599074/system-calls-overhead

          大部分互聯網應用服務,耗時的部分不是計算,而是 I/O。

          減少 I/O wait, 各司其職,專心干 I/O,專心干計算,epoll 批量撈任務,(refer: event driven)

          利用 DMA 減少 CPU 負擔 - 零拷貝 NewI/O Redis SingleThread (even 6.0), Node.js

          避免不必要的調度 - Context Switch

          CPU 親和性,讓 CPU 更加聚焦

          蛻變

          用更高效的數據結構、算法、第三方組件,讓程序本身蛻變。

          從邏輯短路、Map 代替 List 遍歷、減少鎖范圍、這樣的編碼技巧,到應用 FisherYates、Dijkstra 這些經典算法,注意每一行代碼細節,量變會發生質變。更何況某個算法就足以讓系統性能產生一兩個數量級的提升。

          適應

          因地制宜,適應特定的運行環境

          在瀏覽器中主要是優化方向是 I/O、UI 渲染引擎、JS 執行引擎三個方面。

          I/O 越少越好,能用 WebSocket 的地方就不用 Ajax,能用 Ajax 的地方就不要刷整個頁面;

          UI 渲染方面,減少重排和重繪,比如 Vue、React 等 MVVM 框架的虛擬 DOM 用額外的計算換取最精簡的 DOM 操作;

          JS 執行引擎方面,少用動態性極高的寫法,比如 eval、隨意修改對象或對象原型的屬性。

          前端的優化有個神器:Light House,在新版本 Chrome 已經嵌到開發者工具中了,可以一鍵生成性能優化報告,按照優化建議改就完了。

          與瀏覽器環境頗為相似的 Node.js 環境:

          https://segmentfault.com/a/1190000007621011#articleHeader11

          Java

          • C1 C2 JIT 編譯器
          • 棧上分配

          Linux

          • 各種參數優化
          • 內存分配和 GC 策略
          • Linux 內核參數 Brendan Gregg
          • 內存區塊配置(DB,JVM,V8,etc.)

          利用語言特性和運行時環境 - 比如寫出利于 JIT 的代碼

          • 多靜態少動態 - 舍棄動態特性的靈活性 - hardcode/if-else,強類型,弱類型語言避免類型轉換 AOT/JIT vs 解釋器, 匯編,機器碼 GraalVM

          減少內存的分配和回收,少對列表做增加或刪除

          對于 RAM 有限的嵌入式環境,有時候時間不是問題,反而要拿時間換空間,以節約 RAM 的使用。

          運籌

          把眼界放寬,跳出程序和運行環境本身,從整體上進行系統性分析最高性價比的優化方案,分析潛在的優化切入點,以及能夠調配的資源和技術,運籌帷幄。

          其中最簡單易行的幾個辦法,就是花錢,買更好或更多的硬件基礎設施,這往往是開發人員容易忽視的,這里提供一些妙招:

          • 服務器方面,云服務廠商提供各種類型的實例,每種類型有不同的屬性側重,帶寬、CP、磁盤的 I/O 能力,選適合的而不是更貴的
          • 舍棄虛擬機 - Bare Mental,比如神龍服務器
          • 用 ARM 架構 CPU 的服務器,同等價格可以買到更多的服務器,對于多數可以跨平臺運行的服務端系統來說與 x86 區別并不大,ARM 服務器的數據中心也是技術發展趨勢使然
          • 如果必須用 x86 系列的服務器,AMD 也 Intel 的性價比更高。

          第一點非常重要,軟件性能遵循木桶原理,一定要找到瓶頸在哪個硬件資源,把錢花在刀刃上。

          如果是服務端帶寬瓶頸導致的性能問題,升級再多核 CPU 也是沒有用的。

          我有一次性能優化案例:把一個跑復雜業務的 Node.js 服務器從 AWS 的 m4 類型換成 c4 類型,內存只有原來的一半,但 CPU 使用率反而下降了 20%,同時價格還比之前更便宜,一石二鳥。

          這是因為 Node.js 主線程的計算任務只有一個 CPU 核心在干,通過 CPU Profile 的火焰圖,可以定位到該業務的瓶頸在主線程的計算任務上,因此提高單核頻率的作用是立竿見影的。而該業務對內存的消耗并不多,套用一些定制 v8 引擎內存參數的方案,起不了任何作用。

          畢竟這樣的例子不多,大部分時候還是要多花錢買更高配的服務器的,除了這條花錢能直接解決問題的辦法,剩下的辦法難度就大了:

          • 利用更底層的特性實現功能,比如 FFI WebAssembly 調用其他語言,Java Agent Instrument,字節碼生成(BeanCopier, Json Lib),甚至匯編等等
          • 使用硬件提供的更高效的指令
          • 各種提升 TLB 命中率的機制,減少內存的大頁表
          • 魔改 Runtime,Facebook 的 PHP,阿里騰訊定制的 JDK
          • 網絡設備參數,MTU
          • 專用硬件:GPU 加速(cuda)、AES 硬件卡和高級指令加速加解密過程,比如 TLS
          • 可編程硬件:地獄級難度,FPGA 硬件設備加速特定業務
          • NUMA
          • 更宏觀的調度,VM 層面的共享 vCPU,K8S 集群調度,總體上的優化

          小結

          有些手段,是憑空換出來更多的空間和時間了嗎?

          天下沒有免費的午餐,即使那些看起來空手套白狼的優化技術,也需要額外的人力成本來做,副作用可能就是專家級的發際線吧。還好很多復雜的性能優化技術我也不會,所以我本人發際線還可以。

          這一小節總結了一些方向,有些技術細節非常深,這里也無力展開。不過,即使榨干了單機性能,也可能不足以支撐業務,這時候就需要分布式集群出場了,因此后面介紹的 3 個技術方向,都與并行化有關

          影分身術 —— 水平擴容

          本節的水平擴容以及下面一節的分片,可以算整體的性能提升而不是單點的性能優化,會因為引入額外組件反而降低了處理單個請求的性能。

          但當業務規模大到一定程度時,再好的單機硬件也無法承受流量的洪峰,就得水平擴容了,畢竟”眾人拾柴火焰高”。

          在這背后的理論基礎是,硅基半導體已經接近物理極限,隨著摩爾定律的減弱,阿姆達爾定律的作用顯現出來:

          https://en.wikipedia.org/wiki/Amdahl%27s_law

          水平擴容必然引入負載均衡

          • 多副本
          • 水平擴容的前提是無狀態
          • 讀>>寫, 多個讀實例副本 (CDN)
          • 自動擴縮容,根據常用的或自定義的 metrics,判定擴縮容的條件,或根據 CRON
          • 負載均衡策略的選擇

          奧義 —— 分片術

          水平擴容針對無狀態組件,分片針對有狀態組件。二者原理都是提升并行度,但分片的難度更大。

          負載均衡也不再是簡單的加權輪詢了,而是進化成了各個分片的協調器

          • Java1.7 的及之前的 ConcurrentHashMap 分段鎖
          • 有狀態數據的分片
          • 如何選擇 Partition/Sharding Key
          • 負載均衡難題
          • 熱點數據,增強緩存等級,解決分散的緩存帶來的一致性難題
          • 數據冷熱分離,SSD - HDD
          • 分開容易合并難
          • 區塊鏈的優化,分區域

          秘術 —— 無鎖術

          有些業務場景,比如庫存業務,按照正常的邏輯去實現,水平擴容帶來的提升非常有限,因為需要鎖住庫存,扣減,再解鎖庫存。

          票務系統也類似,為了避免超賣,需要有一把鎖禁錮了橫向擴展的能力。

          不管是單機還是分布式微服務,鎖都是制約并行度的一大因素。比如上篇提到的秒殺場景,庫存就那么多,系統超賣了可能導致非常大的經濟損失,但用分布式鎖會導致即使服務擴容了成千上萬個實例,最終無數請求仍然阻塞在分布式鎖這個串行組件上了,再多水平擴展的實例也無用武之地。

          避免競爭 Race Condition 是最完美的解決辦法。

          上篇說的應對秒殺場景,預取庫存就是減輕競態條件的例子,雖然取到服務器內存之后仍然有多線程的鎖,但鎖的粒度更細了,并發度也就提高了。

          • 線程同步鎖
          • 分布式鎖
          • 數據庫鎖 update select 子句
          • 事務鎖
          • 順序與亂序
          • 樂觀鎖/無鎖 CAS Java 1.8 之后的 ConcurrentHashMap
          • pipeline 技術 - CPU 流水線 Redis Pipeline 大數據分析 并行計算
          • TCP 的緩沖區排頭阻塞 QUIC HTTP3.0

          總結

          以 ROI 的視角看軟件開發,初期人力成本的投入,后期的維護成本,計算資源的費用等等,選一個合適的方案而不是一個性能最高的方案。

          本篇結合個人經驗總結了常見的性能優化手段,這些手段只是冰山一角。在初期就設計實現出一個完美的高性能系統是不可能的,隨著軟件的迭代和體量的增大,利用壓測,各種工具(profiling,vmstat,iostat,netstat),以及監控手段,逐步找到系統的瓶頸,因地制宜地選擇優化手段才是正道。

          有利必有弊,得到一些必然會失去一些,有一些手段要慎用。Linux 性能優化大師 Brendan Gregg 一再強調的就是:切忌過早優化、過度優化。

          持續觀測,做 80%高投入產出比的優化。

          除了這些設計和實現時可能用到的手段,在技術選型時選擇高性能的框架和組件也非常重要。

          另外,部署基礎設施的硬件性能也同樣,合適的服務器和網絡等基礎設施往往會事半功倍,比如云服務廠商提供的各種字母開頭的 instance,網絡設備帶寬的速度和穩定性,磁盤的 I/O 能力等等。

          多數時候我們應當使用更高性能的方案,但有時候甚至要故意去違背它們。最后,以《Effective Java》第一章的一句話結束本文吧。

          【編者按】Canvas 與 DOM 是開發者進行瀏覽器渲染時最常用的技術方案,關于這兩門技術的對比文章屢見不鮮,各自擁有一批忠實的開發者。然而,Google 近期的一個決策可能會改變現狀。

          作者 | Matthew MacDonald

          譯者 | 彎月

          出品 | CSDN(ID:CSDNnews)

          Google最近決定使用HTML的<canvas>來渲染Google Docs中的一切,引起了軒然大波。人們的擔憂不無道理。曾幾何時,Web的目標是分享架構化信息,包含合理的元數據,而且易于開展合作。然而,現在卻成了在瀏覽器的沙盒中運行的半透明模型。

          從HTML元素切換到Canvas上的像素渲染,Google的這個決定并非史無前例。很多先進的Web早就突破了傳統Web元素的束縛。Google地圖多年前就開始使用Canvas渲染了。VS Code使用canvas來繪制像素級的終端界面。Google新興的跨平臺UI框架Flutter在瀏覽器中也會默認使用Canvas。

          但這次感覺不一樣。canvas渲染加上WebAssembly等其他技術,點燃了導火索。似乎我們熟悉的那種模式(下載JavaScript代碼并在HTML文檔中執行)只不過是Web開發進化之路上的一個過客而已。

          換一種說法,我們曾理所當然地認為,我們可以看到運行中的代碼,檢查標簽,還可以查看CSS。但是,也許這一切只不過是軟件設計長河中的一段小插曲。

          那么接下來會發生什么?


          Canvas渲染方式越來越流行


          人們總是對Google亦步亦趨。

          大約15年前,Google是異步JavaScript調用(后來稱作Ajax)的先驅。他們主導的這種技術被用到了Gmail和Google地圖中,后來成了Web開發的基礎。現在,Google開始在canvas上畫UI,等于向新一代的Web開發者宣告了這種做法的合理性。

          目前,使用canvas渲染還有著不低的門檻。在Google Docs的構建過程中,Google重新發明了許多人們習以為常的東西,例如精確定位、文本選擇、拼寫檢查、重畫調優等。今天,只有少數幾家公司才會考慮采用canvas渲染來獲得可能的性能提升。

          最大的問題是可訪問性。為了遵守可訪問性的法規(作為像Google這樣的政府供應商來說,合規是必須的,對于希望盡社會責任的企業來說,可訪問性也非常重要),應用程序必須滿足特定的要求。基于canvas的Google Docs依然需要為屏幕閱讀器、屏幕放大鏡、高對比度設置、低敏捷度特性等提供支持。他們的做法之一就是在真正的canvas渲染的內容之外,再專門為輔助工具實現一個不可見的DOM。當然,這兩個模型之間要保持完美的同步。

          目前還沒有現成的標準供開發者在使用了canvas渲染的應用程序中添加可訪問性支持。但是隨著canvas渲染技術的流行,這種情況也會改變,而且很難說會以多快的速度改變。Google越來越多地采用該技術,會給該領域帶來大量的關注、發展和進步。很快就會出現許多庫,然后就會出現標準和API。我們可以給阿特伍德定律加一條:

          “所有能用JavaScript實現的最終都會用JavaScript實現,哪怕需要改進JavaScript。”


          語義Web已死

          代碼混淆的方法有哪些?#

          代碼混淆是一種通過修改代碼結構、變量名、函數名等方式來使代碼難以被理解和分析的技術。代碼混淆通常用于保護代碼的知識產權,以及防止惡意攻擊者對代碼進行逆向工程和分析。

          在 JavaScript 中,代碼混淆通常使用一些工具來實現,其中最流行的是 Webpack。Webpack 是一個現代化的 JavaScript 應用程序打包工具,它可以將多個 JavaScript 文件打包成一個或多個文件,還可以通過代碼混淆來保護代碼。

          Webpack 通過以下幾個步驟來進行代碼混淆:

          1.壓縮代碼:Webpack 會使用 UglifyJS 這樣的工具來壓縮 JavaScript 代碼,刪除空格、注釋等不必要的內容,從而減小文件大小。

          2.混淆變量名:Webpack 會將 JavaScript 中的變量名、函數名等重命名為隨機的字符或數字,使得代碼難以被理解和分析。

          3.刪除無用代碼:Webpack 會刪除代碼中未被使用的變量、函數等內容,從而減小文件大小。

          4.提取公共代碼:Webpack 可以將多個 JavaScript 文件中重復的代碼提取出來,生成一個單獨的文件,從而減小文件大小并提高加載速度。

          5.代碼分割:Webpack 還可以將代碼拆分成多個小塊,實現按需加載,從而提高應用程序的性能和用戶體驗。

          綜上所述,Webpack 的代碼混淆功能主要是通過壓縮代碼、混淆變量名、刪除無用代碼、提取公共代碼和代碼分割等多種技術來實現的。這些技術可以有效地保護 JavaScript 代碼的安全性和知識產權,同時也可以提高應用程序的性能和用戶體驗。

          在使用 Webpack 進行代碼混淆時,需要注意以下幾點:

          1.代碼混淆會使得代碼難以被理解和維護,因此應該謹慎使用,必要時可以開啟 sourcemap 進行源碼調試。

          2.代碼混淆不是萬無一失的,仍然可以被有經驗的攻擊者破解,因此還需要其他安全措施來保護代碼的安全性。

          3.代碼混淆會增加代碼的打包時間,因此在開發過程中應該避免頻繁地進行代碼混淆。

          4.代碼混淆會影響調試過程,因此在開發過程中應該關閉代碼混淆功能,以方便調試。

          綜上所述,代碼混淆是一種重要的保護 JavaScript 代碼安全的技術,Webpack 提供了多種代碼混淆技術,可以根據需求進行選擇和配置。


          主站蜘蛛池模板: 中文字幕aⅴ人妻一区二区| 日本一区二区三区爆乳| 日韩AV无码一区二区三区不卡毛片| 国产精品毛片a∨一区二区三区| 亚洲国产成人一区二区精品区| 亚洲一区二区三区精品视频| av在线亚洲欧洲日产一区二区| 亚洲日本乱码一区二区在线二产线 | 国产精品日本一区二区不卡视频| 精品无码综合一区二区三区 | 水蜜桃av无码一区二区| 精品一区二区三区免费毛片| 无码av免费毛片一区二区| 国产成人AV一区二区三区无码| 538国产精品一区二区在线| 亚洲国产欧美日韩精品一区二区三区 | 一区二区三区在线观看视频| 海角国精产品一区一区三区糖心| 免费无码VA一区二区三区| 国产日韩AV免费无码一区二区| 亚洲AV日韩精品一区二区三区| 国产精品亚洲综合一区在线观看| 无码av免费毛片一区二区| 91video国产一区| 91精品福利一区二区三区野战| 精品久久一区二区三区| 日韩精品无码一区二区视频| 一级特黄性色生活片一区二区| 精品无码一区二区三区水蜜桃| 日韩精品久久一区二区三区 | 亚洲AV无码一区二区三区鸳鸯影院| 人妻AV一区二区三区精品| 无码乱人伦一区二区亚洲一| 中文人妻av高清一区二区| 国产自产V一区二区三区C| 无码一区二区三区| 亚洲综合无码一区二区痴汉| 大香伊人久久精品一区二区 | 一区五十路在线中出| 国产精品第一区第27页| 国产在线无码视频一区二区三区 |