微服務技術棧:流量整形算法,服務熔斷與降級

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、流量控制

1、基本概念

流量控制的核心作用是限制流出某一網絡的某一連接的流量與突發,使這類報文以比較均勻的速度流動發送,達到保護系統相對穩定的目的。通常是將請求放入緩衝區或隊列內,然後基於特定策略處理請求,勻速或者批量處理,該過程也稱流量整形。

流量控制的核心算法有以下兩種:漏桶算法和令牌桶算法。

2、漏桶算法

基礎描述

漏桶算法是流量整形或速率限制時經常使用的一種算法,它的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便為網絡提供一個穩定的流量。

漏桶算法基本思路:請求(水流)先進入到容器(漏桶)里,漏桶以一定的速度出水,這裏就是指流量流出的策略,當流量流入速度過大容器無法承接就會直接溢出,通過該過程限制數據的傳輸速率。

核心要素

通過上述流程,不難發現漏桶算法涉及下面幾個要素:

容器容量

容器的大小直接決定能承接流量的多少,容器一但接近飽和,要麼溢出,要麼加快流速;

流出速度

流量流出的速度取決於服務的請求處理能力,接口支撐的併發越高,流速就可以越大;

時間控制

基於時間記錄,判斷流量流出速度,控制勻速模式,

注意:需要一個基本的判定策略,漏桶算法在系統能承接當前併發流量時,不需要啟用。

3、令牌桶算法

基礎描述

令牌桶可自行以恆定的速率源源不斷地產生令牌。如果令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢出。

令牌桶算法雖然根本目的也是控制流量速度,但是當令牌桶內的令牌足夠多時,則允許流量階段性的併發。傳送到令牌桶的數據包需要消耗令牌。不同大小的數據包,消耗的令牌數量不一樣。

核心要素

令牌桶

存放按照特定的速率生成的令牌,以此控制流量速度。

匹配規則

這裏的匹配規則更多是服務於分佈式系統,例如服務A是系統的核心交易,當出現併發時,基於令牌桶最匹配規則,只允許交易請求通過,例如:常見雙十一期間,各大電商平台提示,為保證核心交易,邊緣服務的數據延遲或暫停等。

注意:令牌桶算法和漏桶算法的目的雖然相同,但是實現策略是相反的,不過都存在一個問題,為保證大部分請求流量成功,會犧牲小部分請求。

二、限流組件

1、Nginx代理組件

Nginx反向代理實際運行方式是指以代理服務器來接收客戶端連接請求,然後將請求轉發給內部網絡上的服務器,並將從服務器上得到的結果返回給客戶端,此時代理服務器對外就表現為一個服務器。

流量限制是Nginx作為代理服務中一個非常實用的功能,通過配置方式來限制用戶在給定時間內HTTP請求的數量,兩個主要的配置指令limit_req_zonelimit_req,以此保護高併發下系統的穩定。

2、CDN邊緣節點

CDN邊緣節點,準確的說並不是用來處理流量限制的,而是存放靜態頁面。內容緩存為CDN網絡節點,位於用戶接入點,是面向最終用戶的內容提供設備,可緩存靜態Web內容和流媒體內容,實現內容的邊緣傳播和存儲,以便用戶的就近訪問,這樣避免用戶大量刷新數據服務器,節省骨幹網帶寬,減少帶寬需求量。

在高併發場景下,尤其是倒計時搶購類似業務,在活動開始前後用戶會產生大量刷新頁面的操作,基於CDN節點,這些請求不會下沉到數據的服務接口上。也可以基於頁面做一些請求攔截,比如點擊頁面單位時間內只放行一定量的請求,以此也可以實現一個限流控制。

三、熔斷器組件

所謂熔斷器機制,即類似電流的保險器,當然電壓過高會自動跳閘,從而保護電路系統。微服務架構中服務保護也是這個策略,當服務被判斷異常,會從服務列表斷開,等待恢復在重新連接。服務熔斷降級的策略實現有如下幾個常用的組件。

1、Hystrix組件

基礎簡介

Hystrix當前處於維護模式,即不再更新,作為SpringCloud微服務組件中,最原生的一個熔斷組件,很多思路還是有必要了解一下。例如:服務熔斷,阻止故障的連鎖反應,快速失敗並迅速恢復,服務降級等。

某個微服務發生故障時,要快速切斷服務,提示用戶,後續請求,不調用該服務,直接返回,釋放資源,這就是服務熔斷。

熔斷器策略

服務器高併發下,壓力劇增的時候,根據當業務情況以及流量,對一些服務和頁面有策略的降級(可以理解為關閉不必要的服務),以此緩解服務器資源的壓力以保障核心任務的正常運行。熔斷生效后,會在指定的時間后調用請求來測試依賴是否恢復,依賴的應用恢復后關閉熔斷。

基本流程:

首先判斷服務熔斷器開關狀態,服務如果未熔斷則放行請求;如果服務處於熔斷中則直接返回。

每次調用都執行兩個函數markSuccess(duration)和markFailure(duration) 來統計在一定的時間段內的調用是成功和失敗次數。

基於上述的成功和失敗次數的計算策略,來判斷是否應該打開熔斷器,如果錯誤率高於一定的閾值,就會觸發熔斷機制。

熔斷器有一個生命周期,周期過後熔斷器器進入半開狀態,允許放行一個試探請求;否則,不允許放行。

2、Sentinel組件

基礎簡介

基於微服務的模式,服務和服務之間的穩定性變得越來越重要。Sentinel以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。

Sentinel可以針對不同的調用關係,以不同的運行指標(如QPS、併發調用數、系統負載等)為基準,收集資源的路徑,並將這些資源的調用路徑以樹狀結構存儲起來,用於根據調用路徑對資源進行流量控制。

流量整形策略

直接拒絕模式是默認的流量控制方式,即請求超出任意規則的閾值后,新的請求就會被立即拒絕。

啟動預熱模式:當流量激增的時候,控制流量通過的速率,讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。

勻速排隊方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶算法。

熔斷策略

Sentinel本質上是基於熔斷器模式,支持基於異常比率的熔斷降級,在調用達到一定量級並且失敗比率達到設定的閾值時自動進行熔斷,此時所有對該資源的調用都會被阻塞,直到過了指定的時間窗口后才啟發性地恢復。

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/husky-spring-cloud
GitEE·地址
https://gitee.com/cicadasmile/husky-spring-cloud

推薦閱讀:微服務架構系列

序號 標題
01 微服務架構:項目技術選型簡介,架構圖解說明
02 微服務架構:業務架構設計,系統分層管理
03 微服務架構:數據庫選型簡介,業務數據規劃設計
04 微服務架構:中間件集成,公共服務封裝
05 微服務架構:SpringCloud 基礎組件應用設計
06 微服務架構:通過業務、應用、技術、存儲,聊聊架構
07 微服務技術棧:常見註冊中心組件,對比分析

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

SQL運行內幕:從執行原理看調優的本質

相信大家看過無數的MySQL調優經驗貼了,會告訴你各種調優手段,如:

  • 避免 select *;
  • join字段走索引;
  • 慎用in和not in,用exists取代in;
  • 避免在where子句中對字段進行函數操作;
  • 盡量避免更新聚集索引;
  • group by如果不需要排序,手動加上 order by null;
  • join選擇小表作為驅動表;
  • order by字段盡量走索引…

其中有些手段也許跟隨者MySQL版本的升級過時了。我們真的需要背這些調優手段嗎?我覺得是沒有必要的,在掌握MySQL存儲架構SQL執行原理的情況下,我們就很自然的明白,為什麼要提議這麼優化了,甚至能夠發現別人提的不太合理的優化手段。

在 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 這篇文章中,我們已經介紹了MySQL的存儲架構,詳細對你在MySQL存儲索引緩衝IO相關的調優經驗中有了一定的其實。

本文,我們重點講解常用的SQL的執行原理,從執行原理,以及MySQL內部對SQL的優化機制,來分析SQL要如何調優,理解為什麼要這樣…那樣…那樣…調優。

如果沒有特別說明,本文以MySQL5.7版本作為講解和演示。

閱讀完本文,您將了解到:

  • COUNT: MyISAM和InnoDB存儲引擎處理count的區別是什麼?
  • COUNT: count為何性能差?
  • COUNT: count有哪些書寫方式,怎麼count統計會快點?
  • ORDER BY: order by語句有哪些排序模式,以及每種排序模式的優缺點?
  • ORDER BY: order by語句會用到哪些排序算法,在什麼場景下會選擇哪種排序算法
  • ORDER BY: 如何查看和分析sql的order by優化手段(執行計劃 + OPTIMIZER_TRACE日誌)
  • ORDER BY: 如何優化order by語句的執行效率?(思想:減小行查詢大小,盡量走索引,能夠走覆蓋索引最佳,可適當增加sort buffer內存大小)
  • JOIN: join走索引的情況下是如何執行的?
  • JOIN: join不走索引的情況下是如何執行的?
  • JOIN: MySQL對Index Nested-Loop Join做了什麼優化?(MMR,BKA)
  • JOIN: BNL算法對緩存會產生什麼影響?有什麼優化策略?
  • JOIN: 有哪些常用的join語句?
  • JOIN: 針對join語句,有哪些優化手段?
  • UNION: union語句執行原理是怎樣的?
  • UNION: union是如何去重的?
  • GROUP BY: group by完全走索引的情況下執行計劃如何?
  • GROUP BY: 什麼情況下group by會用到臨時表?什麼情況下會用到臨時表+排序?
  • GROUP BY: 對group by有什麼優化建議?
  • DISTINCT: distinct關鍵詞執行原理是什麼?
  • 子查詢: 有哪些常見的子查詢使用方式?
  • 子查詢: 常見的子查詢優化有哪些?
  • 子查詢: 真的要盡量使用關聯查詢取代子查詢嗎?
  • 子查詢:in 的效率真的這麼慢嗎?
  • 子查詢: MySQL 5.6之後對子查詢做了哪些優化?(SEMIJOIN,Materializatioin,Exists優化策略)
  • 子查詢: Semijoin有哪些優化策略,其中Materializatioin策略有什麼執行方式,為何要有這兩種執行方式?
  • 子查詢: 除了in轉Exists這種優化優化,MariaDB中的exists轉in優化措施有什麼作用?

1、count

存儲引擎的區別

  • MyISAM引擎每張表中存放了一個meta信息,裡面包含了row_count屬性,內存和文件中各有一份,內存的count變量值通過讀取文件中的count值來進行初始化。[1]但是如果帶有where條件,還是必須得進行表掃描

  • InnoDB引擎執行count()的時候,需要把數據一行行從引擎裏面取出來進行統計。

下面我們介紹InnoDB中的count()。

count中的一致性視圖

InnoDB中為何不像MyISAM那樣維護一個row_count變量呢?

前面 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 一文我們了解到,InnoDB為了實現事務,是需要MVCC支持的。MVCC的關鍵是一致性視圖。一個事務開啟瞬間,所有活躍的事務(未提交)構成了一個視圖數組,InnoDB就是通過這個視圖數組來判斷行數據是否需要undo到指定的版本。

如下圖,假設執行count的時候,一致性視圖得到當前事務能夠取到的最大事務ID DATA_TRX_ID=1002,那麼行記錄中事務ID超過1002都都要通過undo log進行版本回退,最終才能得出最終哪些行記錄是當前事務需要統計的:

row1是其他事務新插入的記錄,當前事務不應該算進去。所以最終得出,當前事務應該統計row2,row3。

執行count會影響其他頁面buffer pool的命中率嗎?

我們知道buffer pool中的LRU算法是是經過改進的,默認情況下,舊子列表(old區)佔3/8,count加載的頁面一直往舊子列表中插入,在舊子列表中淘汰,不會晉陞到新子列表中。所以不會影響其他頁面buffer pool的命中率。

count(主鍵)

count(主鍵)執行流程如下:

  • 執行器請求存儲引擎獲取數據;
  • 為了保證掃描數據量更少,存儲引擎找到最小的那顆索引樹獲取所有記錄,返回記錄的id給到server。返回記錄之前會進行MVCC及其可見性的判斷,只返回當前事務可見的數據;
  • server獲取到記錄之後,判斷id如果不為空,則累加到結果記錄中。

count(1)

count(1)與count(主鍵)執行流程基本一致,區別在於,針對查詢出的每一條記錄,不會取記錄中的值,而是直接返回一個”1″用於統計累加。統計了所有的行。

count(字段)

與count(主鍵)類似,會篩選非空的字段進行統計。如果字段沒有添加索引,那麼會掃描聚集索引樹,導致掃描的數據頁會比較多,效率相對慢點

count(*)

count(*)不會取記錄的值,與count(1)類似。

執行效率對比:count(字段) < count(主鍵) < count(1)

2、order by

以下是我們本節作為演示例子的表,假設我們有如下錶:

索引如下:

對應的idx_d索引結構如下(這裏我們做了一些誇張的手法,讓一個頁數據變小,為了展現在索引樹中的查找流程):

2.1、如何跟蹤執行優化

為了方便分析sql的執行流程,我們可以在當前session中開啟 optimizer_trace:

SET optimizer_trace=’enabled=on’;

然後執行sql,執行完之後,就可以通過以下堆棧信息查看執行詳情了:

SELECT * FROM information_schema.OPTIMIZER_TRACE\G;

以下是

select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 100,2;

的執行結果,其中符合a=3的有8457條記錄,針對order by重點關注以下屬性

"filesort_priority_queue_optimization": {  // 是否啟用優先級隊列
  "limit": 102,           // 排序后需要取的行數,這裏為 limit 100,2,也就是100+2=102
  "rows_estimate": 24576, // 估計參与排序的行數
  "row_size": 123,        // 行大小
  "memory_available": 32768,    // 可用內存大小,即設置的sort buffer大小
  "chosen": true          // 是否啟用優先級隊列
},
...
"filesort_summary": {
  "rows": 103,                // 排序過程中會持有的行數
  "examined_rows": 8457,      // 參与排序的行數,InnoDB層返回的行數
  "number_of_tmp_files": 0,   // 外部排序時,使用的臨時文件數量
  "sort_buffer_size": 13496,  // 內存排序使用的內存大小
  "sort_mode": "sort_key, additional_fields"  // 排序模式
}

2.1.1、排序模式

其中 sort_mode有如下幾種形式:

  • sort_key, rowid:表明排序緩衝區元組包含排序鍵值和原始錶行的行id,排序后需要使用行id進行回表,這種算法也稱為original filesort algorithm(回表排序算法);
  • sort_key, additional_fields:表明排序緩衝區元組包含排序鍵值和查詢所需要的列,排序后直接從緩衝區元組取數據,無需回表,這種算法也稱為modified filesort algorithm(不回表排序);
  • sort_key, packed_additional_fields:類似上一種形式,但是附加的列(如varchar類型)緊密地打包在一起,而不是使用固定長度的編碼。

如何選擇排序模式

選擇哪種排序模式,與max_length_for_sort_data這個屬性有關,這個屬性默認值大小為1024字節:

  • 如果查詢列和排序列佔用的大小超過這個值,那麼會轉而使用sort_key, rowid模式;
  • 如果不超過,那麼所有列都會放入sort buffer中,使用sort_key, additional_fields或者sort_key, packed_additional_fields模式;
  • 如果查詢的記錄太多,那麼會使用sort_key, packed_additional_fields對可變列進行壓縮。

2.1.2、排序算法

基於參与排序的數據量的不同,可以選擇不同的排序算法:

  • 如果排序取的結果很小,小於內存,那麼會使用優先級隊列進行堆排序;

    • 例如,以下只取了前面10條記錄,會通過優先級隊列進行排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 10;
      
  • 如果排序limit n, m,n太大了,也就是說需要取排序很後面的數據,那麼會使用sort buffer進行快速排序

    • 如下,表中a=1的數據又三條,但是由於需要limit到很後面的記錄,MySQL會對比優先級隊列排序和快速排序的開銷,選擇一個比較合適的排序算法,這裏最終放棄了優先級隊列,轉而使用sort buffer進行快速排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;
      
  • 如果參与排序的數據sort buffer裝不下了,那麼我們會一批一批的給sort buffer進行內存快速排序,結果放入排序臨時文件,最終使對所有排好序的臨時文件進行歸併排序,得到最終的結果;

    • 如下,a=3的記錄超過了sort buffer,我們要查找的數據是排序后1000行起,sort buffer裝不下1000行數據了,最終MySQL選擇使用sort buffer進行分批快排,把最終結果進行歸併排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 1000,10;
      

2.2、order by走索引避免排序

執行如下sql:

select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;

我們看一下執行計劃:

發現Extra列為:Using index condition,也就是這裏只走了索引。

執行流程如下圖所示:

通過idx_d索引進行range_scan查找,掃描到4條記錄,然後order by繼續走索引,已經排好序,直接取前面兩條,然後去聚集索引查詢完整記錄,返回最終需要的字段作為查詢結果。這個過程只需要藉助索引。

如何查看和修改sort buffer大小?

我們看一下當前的sort buffer大小:

可以發現,這裏默認配置了sort buffer大小為512k。

我們可以設置這個屬性的大小:

SET GLOBAL sort_buffer_size = 32*1024;

或者

SET sort_buffer_size = 32*1024;

下面我們統一把sort buffer設置為32k

SET sort_buffer_size = 32*1024; 

2.3、排序算法案例

2.3.1、使用優先級隊列進行堆排序

如果排序取的結果很小,並且小於sort buffer,那麼會使用優先級隊列進行堆排序;

例如,以下只取了前面10條記錄:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;

a=3的總記錄數:8520。查看執行計劃:

發現這裏where條件用到了索引,order by limit用到了排序。我們進一步看看執行的optimizer_trace日誌:

"filesort_priority_queue_optimization": {
  "limit": 10,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "chosen": true  // 使用優先級隊列進行排序
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 11,
  "examined_rows": 8520,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 1448,
  "sort_mode": "sort_key, additional_fields"
}

發現這裡是用到了優先級隊列進行排序。排序模式是:sort_key, additional_fields,即先回表查詢完整記錄,把排序需要查找的所有字段都放入sort buffer進行排序。

所以這個執行流程如下圖所示:

  1. 通過where條件a=3掃描到8520條記錄;
  2. 回表查找記錄;
  3. 把8520條記錄中需要的字段放入sort buffer中;
  4. 在sort buffer中進行堆排序;
  5. 在排序好的結果中取limit 10前10條,寫入net buffer,準備發送給客戶端。

2.3.2、內部快速排序

如果排序limit n, m,n太大了,也就是說需要取排序很後面的數據,那麼會使用sort buffer進行快速排序。MySQL會對比優先級隊列排序和歸併排序的開銷,選擇一個比較合適的排序算法。

如何衡量究竟是使用優先級隊列還是內存快速排序?
一般來說,快速排序算法效率高於堆排序,但是堆排序實現的優先級隊列,無需排序完所有的元素,就可以得到order by limit的結果。
MySQL源碼中聲明了快速排序速度是堆排序的3倍,在實際排序的時候,會根據待排序數量大小進行切換算法。如果數據量太大的時候,會轉而使用快速排序。

有如下SQL:

select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;

我們把sort buffer設置為32k:

SET sort_buffer_size = 32*1024; 

其中a=1的記錄有3條。查看執行計劃:

可以發現,這裏where條件用到了索引,order by limit 用到了排序。我們進一步看看執行的optimizer_trace日誌:

"filesort_priority_queue_optimization": {
  "limit": 302,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "strip_additional_fields": {
    "row_size": 57,
    "sort_merge_cost": 33783,
    "priority_queue_cost": 61158,
    "chosen": false  // 對比發現快速排序開銷成本比優先級隊列更低,這裏不適用優先級隊列
  }
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 3,
  "examined_rows": 3,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 32720,
  "sort_mode": "<sort_key, packed_additional_fields>"
}

可以發現這裏最終放棄了優先級隊列,轉而使用sort buffer進行快速排序。

所以這個執行流程如下圖所示:

  1. 通過where條件a=1掃描到3條記錄;
  2. 回表查找記錄;
  3. 把3條記錄中需要的字段放入sort buffer中;
  4. 在sort buffer中進行快速排序
  5. 在排序好的結果中取limit 300, 2第300、301條記錄,寫入net buffer,準備發送給客戶端。

2.3.3、外部歸併排序

當參与排序的數據太多,一次性放不進去sort buffer的時候,那麼我們會一批一批的給sort buffer進行內存排序,結果放入排序臨時文件,最終使對所有排好序的臨時文件進行歸併排序,得到最終的結果。

有如下sql:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;

其中a=3的記錄有8520條。執行計劃如下:

可以發現,這裏where用到了索引,order by limit用到了排序。進一步查看執行的optimizer_trace日誌:

"filesort_priority_queue_optimization": {
  "limit": 1010,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "strip_additional_fields": {
    "row_size": 57,
    "chosen": false,
    "cause": "not_enough_space"  // sort buffer空間不夠,無法使用優先級隊列進行排序了
  }
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 8520,
  "examined_rows": 8520,
  "number_of_tmp_files": 24,  // 用到了24個外部文件進行排序
  "sort_buffer_size": 32720,
  "sort_mode": "<sort_key, packed_additional_fields>"
}

我們可以看到,由於limit 1000,要返回排序后1000行以後的記錄,顯然sort buffer已經不能支撐這麼大的優先級隊列了,所以轉而使用sort buffer內存排序,而這裏需要在sort buffer中分批執行快速排序,得到多個排序好的外部臨時文件,最終執行歸併排序。(外部臨時文件的位置由tmpdir參數指定)

其流程如下圖所示:

2.4、排序模式案例

2.4.1、sort_key, additional_fields模式

sort_key, additional_fields,排序緩衝區元組包含排序鍵值和查詢所需要的列(先回表取需要的數據,存入排序緩衝區中),排序后直接從緩衝區元組取數據,無需再次回表。

上面 2.3.1、2.3.2節的例子都是這種排序模式,就不繼續舉例了。

2.4.2、<sort_key, packed_additional_fields>模式

sort_key, packed_additional_fields:類似上一種形式,但是附加的列(如varchar類型)緊密地打包在一起,而不是使用固定長度的編碼。

上面2.3.3節的例子就是這種排序模式,由於參与排序的總記錄大小太大了,因此需要對附加列進行緊密地打包操作,以節省內存。

2.4.3、<sort_key, rowid>模式

前面我們提到,選擇哪種排序模式,與max_length_for_sort_data[2]這個屬性有關,max_length_for_sort_data規定了排序行的最大大小,這個屬性默認值大小為1024字節:

也就是說如果查詢列和排序列佔用的大小小於這個值,這個時候會走sort_key, additional_fields或者sort_key, packed_additional_fields算法,否則,那麼會轉而使用sort_key, rowid模式。

現在我們特意把這個值設置小一點,模擬sort_key, rowid模式:

SET max_length_for_sort_data = 100;

這個時候執行sql:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;

這個時候再查看sql執行的optimizer_trace日誌:

"filesort_priority_queue_optimization": {
  "limit": 10,
  "rows_estimate": 27033,
  "row_size": 49,
  "memory_available": 32768,
  "chosen": true
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 11,
  "examined_rows": 8520,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 632,
  "sort_mode": "<sort_key, rowid>"
}

可以發現這個時候切換到了sort_key, rowid模式,在這個模式下,執行流程如下:

  1. where條件a=3掃描到8520條記錄;
  2. 回表查找記錄;
  3. 找到這8520條記錄的idd字段,放入sort buffer中進行堆排序;
  4. 排序完成后,取前面10條;
  5. 取這10條的id回表查詢需要的a,b,c,d字段值;
  6. 依次返回結果給到客戶端。

可以發現,正因為行記錄太大了,所以sort buffer中只存了需要排序的字段和主鍵id,以時間換取空間,最終排序完成,再次從聚集索引中查找到所有需要的字段返回給客戶端,很明顯,這裏多了一次回表操作的磁盤讀,整體效率上是稍微低一點的。

2.5、order by優化總結

根據以上的介紹,我們可以總結出以下的order by語句的相關優化手段:

  • order by字段盡量使用固定長度的字段類型,因為排序字段不支持壓縮;
  • order by字段如果需要用可變長度,應盡量控制長度,道理同上;
  • 查詢中盡量不用用select *,避免查詢過多,導致order by的時候sort buffer內存不夠導致外部排序,或者行大小超過了max_length_for_sort_data導致走了sort_key, rowid排序模式,使得產生了更多的磁盤讀,影響性能;
  • 嘗試給排序字段和相關條件加上聯合索引,能夠用到覆蓋索引最佳。

3、join

為了演示join,接下來我們需要用到這兩個表:

CREATE TABLE `t30` ( 
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  `c` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY idx_a(a)
) ENGINE=InnoDB CHARSET=utf8mb4;

CREATE TABLE `t31` ( 
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  `f` int(11) NOT NULL,
  `c` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY idx_a(a)
) ENGINE=InnoDB CHARSET=utf8mb4;

insert into t30(a,b,c) values(1, 1, 1),(12,2,2),(3,3,3),(11, 12, 31),(15,1,32),(33,33,43),(5,13,14),(4,13,14),(16,13,14),(10,13,14);

insert into t31(a,f,c) values(1, 1, 1),(21,2,2),(3,3,3),(12, 1, 1),(31,20,2),(4,10,3),(2,23,24),(22,23,24),(5,23,24),(20,23,24);

在MySQL官方文檔中 8.8.2 EXPLAIN Output Format[3] 提到:MySQL使用Nested-Loop Loin算法處理所有的關聯查詢。使用這種算法,意味着這種執行模式:

  • 從第一個表中讀取一行,然後在第二個表、第三個表…中找到匹配的行,以此類推;
  • 處理完所有關聯的表后,MySQL將輸出選定的列,如果列不在當前關聯的索引樹中,那麼會進行回表查找完整記錄;
  • 繼續遍歷,從表中取出下一行,重複以上步驟。

下面我們所講到的都是Nested-Loop Join算法的不同實現。

多表join:不管多少個表join,都是用的Nested-Loop Join實現的。如果有第三個join的表,那麼會把前兩個表的join結果集作為循環基礎數據,在執行一次Nested-Loop Join,到第三個表中匹配數據,更多多表同理。

3.1、join走索引(Index Nested-Loop Join)

3.1.1、Index Nested-Loop Join

我們執行以下sql:

select * from t30 straight_join t31 on t30.a=t31.a;

查看執行計劃:

可以發現:

  • t30作為驅動表,t31作為被驅動表;
  • 通過a字段關聯,去t31表查找數據的時候用到了索引。

該sql語句的執行流程如下圖:

  1. 首先遍歷t30聚集索引;
  2. 針對每個t30的記錄,找到a的值,去t31的idx_a索引中找是否存在記錄;
  3. 如果存在則拿到t30對應索引記錄的id回表查找完整記錄;
  4. 分別取t30和t31的所有字段作為結果返回。

由於這個過程中用到了idx_a索引,所以這種算法也稱為:Index Nested-Loop (索引嵌套循環join)。其偽代碼結構如下:

// A 為t30聚集索引
// B 為t31聚集索引
// BIndex 為t31 idx_a索引
void indexNestedLoopJoin(){
  List result;
  for(a in A) {
    for(bi in BIndex) {
      if (a satisfy condition bi) {
        output <a, b>;
      }
    }
  }
}

假設t30記錄數為m,t31記錄數為n,每一次查找索引樹的複雜度為log2(n),所以以上場景,總的複雜度為:m + m*2*log2(n)

也就是說驅動表越小,複雜度越低,越能提高搜索效率。

3.1.2、Index nested-Loop Join的優化

我們可以發現,以上流程,每次從驅動表取一條數據,然後去被驅動表關聯取數,表現為磁盤的隨記讀,效率是比較低低,有沒有優化的方法呢?

這個就得從MySQL的MRR(Multi-Range Read)[4]優化機制說起了。

3.1.2.1、Multi-Range Read優化

我們執行以下代碼,強制開啟MMR功能:

set optimizer_switch="mrr_cost_based=off"

然後執行以下SQL,其中a是索引:

select * from t30 force index(idx_a) where a<=12 limit 10;

可以得到如下執行計劃:

可以發現,Extra列提示用到了MRR優化。

這裏為了演示走索引的場景,所以加了force index關鍵詞。

正常不加force index的情況下,MySQL優化器會檢查到這裏即使走了索引還是需要回表查詢,並且表中的數據量不多,那乾脆就直接掃描全表,不走索引,效率更加高了。

如果沒有MRR優化,那麼流程是這樣的:

  1. 在idx_a索引中找到a<10的記錄;
  2. 取前面10條,拿着id去回表查找完整記錄,這裏回表查詢是隨機讀,效率較差
  3. 取到的結果通過net buffer返回給客戶端。

使用了MRR優化之後,這個執行流程是這樣的:

  1. 在idx_abc索引中找到a<10的記錄;
  2. 取10條,把id放入read rnd buffer;
  3. read rnd buffer中的id排序;
  4. 排序之後回表查詢完整記錄,id越多,排好序之後越有可能產生連續的id,去磁盤順序讀;
  5. 查詢結果寫入net buffer返回給客戶端;

3.1.2.2、Batched Key Access

與Multi-Range Read的優化思路類似,MySQL也是通過把隨機讀改為順序讀,讓Index Nested-Loop Join提升查詢效率,這個算法稱為Batched Key Access(BKA)[5]算法。

我們知道,默認情況下,是掃描驅動表,一行一行的去被驅動表匹配記錄。這樣就無法觸發MRR優化了,為了能夠觸發MRR,於是BKA算法登場了。

在BKA算法中,驅動表通過使用join buffer批量在被驅動表輔助索引中關聯匹配數據,得到一批結果,一次性傳遞個數據庫引擎的MRR接口,從而可以利用到MRR對磁盤讀的優化。

為了啟用這個算法,我們執行以下命令(BKA依賴於MRR):

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

我們再次執行以下關聯查詢sql:

select * from t30 straight_join t31 on t30.a=t31.a;

我們可以得到如下的執行計劃:

可以發現,這裏用到了:Using join buffer(Batched Key Access)

執行流程如下:

  1. 把驅動表的數據批量放入join buffer中;
  2. 在join buffer中批與被驅動表的輔助索引匹配結果,得到一個結果集;
  3. 把上一步的結果集批量提交給引擎的MRR接口;
  4. MRR接口處理同上一節,主要進行了磁盤順序讀的優化;
  5. 組合輸出最終結果,可以看到,這裏的結果與沒有開啟BKA優化的順序有所不同,這裏使用了t31被驅動表的id排序作為輸出順序,因為最後一步對被驅動表t31讀取進行MRR優化的時候做了排序。

如果join條件沒走索引,又會是什麼情況呢,接下來我們嘗試執行下對應的sql。

3.2、join不走索引(Block Nested-Loop Join)

3.2.1、Block Nested-Loop Join (BNL)

我們執行以下sql:

select * from t30 straight_join t31 on t30.c=t31.c;

查看執行計劃:

可以發現:

  • t30作為驅動表,t31作為被驅動表;
  • 通過c字段關聯,去t31表查找數據的時候沒有用到索引;
  • join的過程中用到了join buffer,這裏提示用到了Block Nested Loop Join;

該語句的執行流程如下圖:

  1. t30驅動表中的數據分批(分塊)存入join buffer,如果一次可以全部存入,則這裡會一次性存入;
  2. t31被驅動表中掃描記錄,依次取出與join buffer中的記錄對比(內存中對比,快),判斷是否滿足c相等的條件;
  3. 滿足條件的記錄合併結果輸出到net buffer中,最終傳輸給客戶端。

然後清空join buffer,存入下一批t30的數據,重複以上流程。

顯然,每批數據都需要掃描一遍被驅動表,批次越多,掃描越多,但是內存判斷總次數是不變的。所以總批次越小,越高效。所以,跟上一個算法一樣,驅動表越小,複雜度越低,越能提高搜索效率。

3.2.2、BNL問題

在 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 一文中,我們介紹了MySQL Buffer Pool的LRU算法,如下:

默認情況下,同一個數據頁,在一秒鐘之後再次訪問,那麼就會晉陞到新子列表(young區)。

恰巧,如果我們用到了BNL算法,那麼分批執行的話,就會重複掃描被驅動表去匹配每一個批次了。

考慮以下兩種會影響buffer pool的場景:

  • 如果這個時候join掃描了一個很大的冷表,那麼在join這段期間,會持續的往舊子列表(old區)寫數據頁,淘汰隊尾的數據頁,這會影響其他業務數據頁晉陞到新子列表,因為很可能在一秒內,其他業務數據就從舊子列表中被淘汰掉了;
  • 而如果這個時候BNL算法把驅動表分為了多個批次,每個批次掃描匹配被驅動表,都超過1秒鐘,那麼這個時候,被驅動表的數據頁就會被晉陞到新子列表,這個時候也會把其他業務的數據頁提前從新子列表中淘汰掉。

3.2.3、BNL問題解決方案

3.2.3.1、調大 join_buffer_size

針對以上這種場景,為了避免影響buffer pool,最直接的辦法就是增加join_buffer_size的值,以減少對被驅動表的掃描次數。

3.2.3.2、把BNL轉換為BKA

我們可以通過把join的條件加上索引,從而避免了BNL算法,轉而使用BKA算法,這樣也可以加快記錄的匹配速度,以及從磁盤讀取被驅動表記錄的速度。

3.2.3.3、通過添加臨時表

有時候,被驅動表很大,但是關聯查詢又很少使用,直接給關聯字段加索引太浪費空間了,這個時候就可以通過把被驅動表的數據放入臨時表,在零時表中添加索引的方式,以達成3.2.3.2的優化效果。

3.2.3.4、使用hash join

什麼是hash join呢,簡單來說就是這樣的一種模型:

把驅動表滿足條件的數據取出來,放入一個hash結構中,然後把被驅動表滿足條件的數據取出來,一行一行的去hash結構中尋找匹配的數據,依次找到滿足條件的所有記錄。

一般情況下,MySQL的join實現都是以上介紹的各種nested-loop算法的實現,但是從MySQL 8.0.18[6]開始,我們可以使用hash join來實現表連續查詢了。感興趣可以進一步閱讀這篇文章進行了解:[Hash join in MySQL 8 | MySQL Server Blog](https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does.)

3.3、各種join

我們在平時工作中,會遇到各種各樣的join語句,主要有如下:

INNER JOIN

LEFT JOIN

RIGHT JOIN

FULL OUTER JOIN

LEFT JOIN EXCLUDING INNER JOIN

RIGHT JOIN EXCLUDING INNER JOIN

OUTER JOIN EXCLUDING INNER JOIN

更詳細的介紹,可以參考:

  • MySQL JOINS Tutorial: INNER, OUTER, LEFT, RIGHT, CROSS[7]
  • How the SQL join actually works?[8]

3.3、join使用總結

  • join優化的目標是盡可能減少join中Nested-Loop的循環次數,所以請讓小表做驅動表;
  • 關聯字段盡量走索引,這樣就可以用到Index Nested-Loop Join了;
  • 如果有order by,請使用驅動表的字段作為order by,否則會使用 using temporary;
  • 如果不可避免要用到BNL算法,為了減少被驅動表多次掃描導致的對Buffer Pool利用率的影響,那麼可以嘗試把 join_buffer_size調大;
  • 為了進一步加快BNL算法的執行效率,我們可以給關聯條件加上索引,轉換為BKA算法;如果加索引成本較高,那麼可以通過臨時表添加索引來實現;
  • 如果您使用的是MySQL 8.0.18,可以嘗試使用hash join,如果是較低版本,也可以自己在程序中實現一個hash join。

4、union

通過使用union可以把兩個查詢結果合併起來,注意:

union all不會去除重複的行,union則會去除重複讀的行。

4.1、union all

執行下面sql:

(select id from t30 order by id desc limit 10) union all (select c from t31 order by id desc limit 10)

該sql執行計劃如下圖:

執行流程如下:

  1. 從t30表查詢出結果,直接寫出到net buffer,傳回給客戶端;
  2. 從331表查詢出結果,直接寫出到net buffer,傳回給客戶端。

4.2、union

執行下面sql:

(select id from t30 order by id desc limit 10) union (select c from t31 order by id desc limit 10)

該sql執行計劃如下圖:

執行流程如下:

  1. 從t30查詢出記錄,寫入到臨時表;
  2. 從t30查詢出記錄,寫入臨時表,在臨時表中通過唯一索引去重;
  3. 把臨時表的數據通過net buffer返回給客戶端。

5、group by

5.1、完全走索引

我們給t30加一個索引:

alter table t30 add index idx_c(c);

執行以下group bysql:

select c, count(*) from t30 group by c;

執行計劃如下:

發現這裏只用到了索引,原因是idx_c索引本身就是按照c排序好的,那麼直接順序掃描idx_c索引,可以直接統計到每一個c值有多少條記錄,無需做其他的統計了。

5.2、臨時表

現在我們把剛剛的idx_c索引給刪掉,執行以下sql:

select c, count(*) from t30 group by c order by null;

為了避免排序,所以我們這裏添加了 order by null,表示不排序。

執行計劃如下:

可以發現,這裏用到了內存臨時表。其執行流程如下:

  1. 掃描t30聚集索引;
  2. 建立一個臨時表,以字段c為主鍵,依次把掃描t30的記錄通過臨時表的字段c進行累加;
  3. 把最後累加得到的臨時表返回給客戶端。

5.3、臨時表 + 排序

如果我們把上一步的order by null去掉,默認情況下,group by的結果是會通過c字段排序的。我們看看其執行計劃:

可以發現,這裏除了用到臨時表,還用到了排序。

我們進一步看看其執行的OPTIMIZER_TRACE日誌:

"steps": [
  {
    "creating_tmp_table": {
      "tmp_table_info": {
        "table": "intermediate_tmp_table",  // 創建中間臨時表
        "row_length": 13,
        "key_length": 4,
        "unique_constraint": false,
        "location": "memory (heap)",
        "row_limit_estimate": 1290555
      }
    }
  },
  {
    "filesort_information": [
      {
        "direction": "asc",
        "table": "intermediate_tmp_table",
        "field": "c"
      }
    ],
    "filesort_priority_queue_optimization": {
      "usable": false,
      "cause": "not applicable (no LIMIT)" // 由於沒有 limit,不採用優先級隊列排序
    },
    "filesort_execution": [
    ],
    "filesort_summary": {
      "rows": 7,
      "examined_rows": 7,
      "number_of_tmp_files": 0,
      "sort_buffer_size": 344,
      "sort_mode": "<sort_key, rowid>"  // rowid排序模式
    }
  }
]

通過日誌也可以發現,這裏用到了中間臨時表,由於沒有limit限制條數,這裏沒有用到優先級隊列排序,這裏的排序模式為sort_key, rowid。其執行流程如下:

  1. 掃描t30聚集索引;
  2. 建立一個臨時表,以字段c為主鍵,依次把掃描t30的記錄通過臨時表的字段c進行累加;
  3. 把得到的臨時表放入sort buffer進行排序,這裏通過rowid進行排序;
  4. 通過排序好的rowid回臨時表查找需要的字段,返回給客戶端。

臨時表是存放在磁盤還是內存?

tmp_table_size 參數用於設置內存臨時表的大小,如果臨時表超過這個大小,那麼會轉為磁盤臨時表:

可以通過以下sql設置當前session中的內存臨時表大小:SET tmp_table_size = 102400;

5.5、直接排序

查看官方文檔的 SELECT Statement[9],可以發現SELECT後面可以使用許多修飾符來影響SQL的執行效果:

SELECT
    [ALL | DISTINCT | DISTINCTROW ]
    [HIGH_PRIORITY]
    [STRAIGHT_JOIN]
    [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
    [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
    select_expr [, select_expr] ...
    [into_option]
    [FROM table_references
      [PARTITION partition_list]]
    [WHERE where_condition]
    [GROUP BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [HAVING where_condition]
    [ORDER BY {col_name | expr | position}
      [ASC | DESC], ...]
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]
    [PROCEDURE procedure_name(argument_list)]
    [into_option]
    [FOR UPDATE | LOCK IN SHARE MODE]

into_option: {
    INTO OUTFILE 'file_name'
        [CHARACTER SET charset_name]
        export_options
  | INTO DUMPFILE 'file_name'
  | INTO var_name [, var_name] ...
}

這裏我們重點關注下這兩個:

  • SQL_BIG_RESULT:可以在包含group by 和distinct的SQL中使用,提醒優化器查詢數據量很大,這個時候MySQL會直接選用磁盤臨時表取代內存臨時表,避免執行過程中發現內存不足才轉為磁盤臨時表。這個時候更傾向於使用排序取代二維臨時表統計結果。後面我們會演示這樣的案例;
  • SQL_SMALL_RESULT:可以在包含group by 和distinct的SQL中使用,提醒優化器數據量很小,提醒優化器直接選用內存臨時表,這樣會通過臨時表統計,而不是排序。

當然,在平時工作中,不是特定的調優場景,以上兩個修飾符還是比較少用到的。

接下來我們就通過例子來說明下使用了SQL_BIG_RESULT修飾符的SQL執行流程。

有如下SQL:

select SQL_BIG_RESULT c, count(*) from t30 group by c;

執行計劃如下:

可以發現,這裏只用到了排序,沒有用到索引或者臨時表。這裏用到了SQL_BIG_RESULT修飾符,告訴優化器group by的數據量很大,直接選用磁盤臨時表,但磁盤臨時表存儲效率不高,最終優化器使用數組排序的方式來完成這個查詢。(當然,這個例子實際的結果集並不大,只是作為演示用)

其執行結果如下:

  1. 掃描t30表,逐行的把c字段放入sort buffer;
  2. 在sort buffer中對c字段進行排序,得到一個排序好的c數組;
  3. 遍歷這個排序好的c數組,統計結果並輸出。

5.4、group by 優化建議

  • 盡量讓group by走索引,能最大程度的提高效率;
  • 如果group by結果不需要排序,那麼可以加上group by null,避免進行排序;
  • 如果group by的數據量很大,可以使用SQL_BIG_RESULT修飾符,提醒優化器應該使用排序算法得到group的結果。

6、distinct[10]

在大多數情況下,DISTINCT可以考慮為GROUP BY的一個特殊案例,如下兩個SQL是等效的:

select distinct a, b, c from t30;

select a, b, c from t30 group by a, b, c order by null;

這兩個SQL的執行計劃如下:

由於這種等效性,適用於Group by的查詢優化也適用於DISTINCT。

區別:distinct是在group by之後的每組中取出一條記錄,distinct分組之後不進行排序。

6.1、Extra中的distinct

在一個關聯查詢中,如果您只是查詢驅動表的列,並且在驅動表的列中聲明了distinct關鍵字,那麼優化器會進行優化,在被驅動表中查找到匹配的第一行時,將停止繼續掃描。如下SQL:

explain select distinct t30.a  from t30, t31 where t30.c=t30.c;

執行計劃如下,可以發現Extra列中有一個distinct,該標識即標識用到了這種優化[10:1]

7、子查詢

首先,我們來明確幾個概念:

子查詢:可以是嵌套在另一個查詢(select insert update delete)內,子查詢也可以是嵌套在另一個子查詢裏面。

MySQL子查詢稱為內部查詢,而包含子查詢的查詢稱為外部查詢。子查詢可以在使用表達式的任何地方使用。

接下來我們使用以下錶格來演示各種子查詢:

create table class (
  id bigint not null auto_increment,
  class_num varchar(10) comment '課程編號',
  class_name varchar(100) comment '課程名稱',
  pass_score integer comment '課程及格分數',
  primary key (id)
) comment '課程';

create table student_class (
  id bigint not null auto_increment,
  student_name varchar(100) comment '學生姓名',
  class_num varchar(10) comment '課程編號',
  score integer comment '課程得分',
  primary key (id)
) comment '學生選修課程信息';

insert into class(class_num, class_name, pass_score) values ('C001','語文', 60),('C002','數學', 70),('C003', '英文', 60),('C004', '體育', 80),('C005', '音樂', 60),('C006', '美術', 70);

insert into student_class(student_name, class_num, score) values('James', 'C001', 80),('Talor', 'C005', 75),('Kate', 'C002', 65),('David', 'C006', 82),('Ann', 'C004', 88),('Jan', 'C003', 70),('James', 'C002', 97), ('Kate', 'C005', 90), ('Jan', 'C005', 86), ('Talor', 'C006', 92);

子查詢的用法比較多,我們先來列舉下有哪些子查詢的使用方法。

7.1、子查詢的使用方法

7.1.1、where中的子查詢

7.1.1.1、比較運算符

可以使用比較運算法,例如=,>,<將子查詢返回的單個值與where子句表達式進行比較,如

查找學生選擇的編號最大的課程信息:

SELECT class.* FROM class WHERE class.class_num = ( SELECT MAX(class_num) FROM student_class );

7.1.1.2、in和not in

如果子查詢返回多個值,則可以在WHERE子句中使用其他運算符,例如IN或NOT IN運算符。如

查找學生都選擇了哪些課程:

SELECT class.* FROM class WHERE class.class_num IN ( SELECT DISTINCT class_num FROM student_class );

7.1.2、from子查詢

在FROM子句中使用子查詢時,從子查詢返回的結果集將用作臨時表。該表稱為派生表或實例化子查詢。如 查找最熱門和最冷門的課程分別有多少人選擇:

SELECT max(count), min(count) FROM (SELECT class_num, count(1) as count FROM student_class group by class_num) as t1;

7.1.3、關聯子查詢

前面的示例中,您注意到子查詢是獨立的。這意味着您可以將子查詢作為獨立查詢執行。

獨立子查詢不同,關聯子查詢是使用外部查詢中的數據的子查詢。換句話說,相關子查詢取決於外部查詢。對於外部查詢中的每一行,對關聯子查詢進行一次評估。

下面是比較運算符中的一個關聯子查詢。

查找每門課程超過平均分的學生課程記錄:

SELECT t1.* FROM student_class t1 WHERE t1.score > ( SELECT AVG(score) FROM student_class t2 WHERE t1.class_num = t2.class_num);

關聯子查詢中,針對每一個外部記錄,都需要執行一次子查詢,因為每一條外部記錄的class_num可能都不一樣。

7.1.3.1、exists和not exists

當子查詢與EXISTS或NOT EXISTS運算符一起使用時,子查詢將返回布爾值TRUE或FALSE。

查找所有學生總分大於100分的課程:

select * from class t1 
where exists(
  select sum(score) as total_score from student_class t2 
  where t2.class_num=t1.class_num group by t2.class_num having total_score > 100
)

7.2、子查詢的優化

上面我們演示了子查詢的各種用法,接下來,我們來講一下子查詢的優化[11]

子查詢主要由以下三種優化手段:

  • Semijoin,半連接轉換,把子查詢sql自動轉換為semijion;
  • Materialization,子查詢物化;
  • EXISTS策略,in轉exists;

其中Semijoin只能用於IN,= ANY,或者EXISTS的子查詢中,不能用於NOT IN,<> ALL,或者NOT EXISTS的子查詢中。

下面我們做一下詳細的介紹。

真的要盡量使用關聯查詢取代子查詢嗎?

在《高性能MySQL》[12]一書中,提到:優化子查詢最重要的建議就是盡可能使用關聯查詢代替,但是,如果使用的是MySQL 5.6或者更新版本或者MariaDB,那麼就可以直接忽略這個建議了。因為這些版本對子查詢做了不少的優化,後面我們會重點介紹這些優化。

in的效率真的這麼慢嗎?

在MySQL5.6之後是做了不少優化的,下面我們就逐個來介紹。

7.2.1、Semijoin

Semijoin[13],半連接,所謂半連接,指的是一張表在另一張表棧道匹配的記錄之後,返回第一張表的記錄。即使右邊找到了幾條匹配的記錄,也最終返回左邊的一條。

所以,半連接非常適用於查找兩個表之間是否存在匹配的記錄,而不關注匹配了多少條記錄這種場景。

半連接通常用於IN或者EXISTS語句的優化。

7.2.1.1、優化場景

上面我們講到:接非常適用於查找兩個表之間是否存在匹配的記錄,而不關注匹配了多少條記錄這種場景。

in關聯子查詢

這種場景,如果使用in來實現,可能會是這樣:

SELECT class_num, class_name
    FROM class
    WHERE class_num IN
        (SELECT class_num FROM student_class where condition);

在這裏,優化器可以識別出IN子句要求子查詢僅從student_class表返回唯一的class_num。在這種情況下,查詢會自動優化為使用半聯接。

如果使用exists來實現,可能會是這樣:

SELECT class_num, class_name
    FROM class
    WHERE EXISTS
        (SELECT * FROM student_class WHERE class.class_num = student_class.class_num);

優化案例

統計有學生分數不及格的課程:

SELECT t1.class_num, t1.class_name
    FROM class t1
    WHERE t1.class_num IN
        (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);

我們可以通過執行以下腳本,查看sql做了什麼優化:

explain extended SELECT t1.class_num, t1.class_name FROM class t1 WHERE t1.class_num IN         (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
show warnings\G;

得到如下執行執行計劃,和SQL重寫結果:

從這個SQL重寫結果中,可以看出,最終子查詢變為了semi join語句:

/* select#1 */ select `test`.`t1`.`class_num` AS `class_num`,`test`.`t1`.`class_name` AS `class_name` 
from `test`.`class` `t1` 
semi join (`test`.`student_class` `t2`) where ((`test`.`t2`.`class_num` = `test`.`t1`.`class_num`) and (`test`.`t2`.`score` < `test`.`t1`.`pass_score`))

而執行計劃中,我們看Extra列:

Using where; FirstMatch(t1); Using join buffer (Block Nested Loop)

Using join buffer這項是在join關聯查詢的時候會用到,前面講join語句的時候已經介紹過了,現在我們重點看一下FirstMatch(t1)這個優化項。

FirstMatch(t1)是Semijoin優化策略中的一種。下面我們詳細介紹下Semijoin有哪些優化策略。

7.2.1.2、Semijoin優化策略

MySQL支持5中Semijoin優化策略,下面逐一介紹。

7.2.1.2.1、FirstMatch

在內部表尋找與外部表匹配的記錄,一旦找到第一條,則停止繼續匹配

案例 – 統計有學生分數不及格的課程:

SELECT t1.class_num, t1.class_name
    FROM class t1
    WHERE t1.class_num IN
        (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);

執行計劃:

執行流程,圖比較大,請大家放大觀看:

  1. 掃描class表,把class表分批放入join buffer中,分批處理;
  2. 在批次中依次取出每一條記錄,在student_class表中掃描查找符合條件的記錄,如果找到,則立刻返回,並從該條匹配的class記錄取出查詢字段返回;
  3. 依次繼續掃描遍歷。

您也可以去MariaDB官網,查看官方的FirstMatch Strategy[14]解釋。

7.2.1.2.2、Duplicate Weedout

將Semijoin作為一個常規的inner join,然後通過使用一個臨時表去重。

具體演示案例,參考MariaDB官網:DuplicateWeedout Strategy[15],以下是官網例子的圖示:

可以看到,灰色區域為臨時表,通過臨時表唯一索引進行去重。

7.2.1.2.3、LooseScan

把內部表的數據基於索引進行分組,取每組第一條數據進行匹配。

具體演示案例,參考MariaDB官網:LooseScan Strategy[16],以下是官網例子的圖示:

7.2.1.4、Materialization[17]

如果子查詢是獨立的(非關聯子查詢),則優化器可以選擇將獨立子查詢產生的結果存儲到一張物化臨時表中。

為了觸發這個優化,我們需要往表裡面添加多點數據,好讓優化器認為這個優化是有價值的。

我們執行以下SQL:

select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';

執行流程如下:

  1. 執行子查詢:通過where條件從student_class 表中找出符合條件的記錄,把所有記錄放入物化臨時表;
  2. 通過where條件從class表中找出符合條件的記錄,與物化臨時表進行join操作。

物化表的唯一索引

MySQL會報物化子查詢所有查詢字段組成一個唯一索引,用於去重。如上面圖示,灰色連線的兩條記錄衝突去重了。

join操作可以從兩個方向執行:

  • 從物化表關聯class表,也就是說,掃描物化表,去與class表記錄進行匹配,這種我們稱為Materialize-scan
  • 從class表關聯物化表,也就是,掃描class表,去物化表中查找匹配記錄,這種我們稱為Materialize-lookup,這個時候,我們用到了物化表的唯一索引進行查找,效率會很快。

下面我們介紹下這兩種執行方式。

Materialize-lookup

還是以上面的sql為例:

select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';

執行計劃如下:

可以發現:

  • t2表的select_type為MATERIALIZED,這意味着id=2這個查詢結果將存儲在物化臨時表中。並把該查詢的所有字段作為臨時表的唯一索引,防止插入重複記錄;
  • id=1的查詢接收一個subquery2的表名,這個表正式我們從id=2的查詢得到的物化表。
  • id=1的查詢首先掃描t1表,依次拿到t1表的每一條記錄,去subquery2執行eq_ref,這裏用到了auto_key,得到匹配的記錄。

也就是說,優化器選擇了對t1(class)表進行全表掃描,然後去物化表進行所以等值查找,最終得到結果。

執行模型如下圖所示:

原則:小表驅動大表,關聯字段被驅動表添加索引

如果子查詢查出來的物化表很小,而外部表很大,並且關聯字段是外部表的索引字段,那麼優化器會選擇掃描物化表去關聯外部表,也就是Materialize-scan,下面演示這個場景。

Materialize-scan

現在我們嘗試給class表添加class_num唯一索引:

alter table class add unique uk_class_num(class_num);

並且在class中插入更多的數據。然後執行同樣的sql,得到以下執行計劃:

可以發現,這個時候id=1的查詢是選擇了subquery2,也就是物化表進行掃描,掃描結果逐行去t1表(class)進行eq_ref匹配,匹配過程中用到了t1表的索引。

這裏的執行流程正好與上面的相反,選擇了從class表關聯物化表。

現在,我問大家:Materialization策略什麼時候會選擇從外部表關聯內部表?相信大家心裏應該有答案了。

執行模型如下:

原則:小表驅動大表,關聯字段被驅動表添加索引

現在留給大家另一個問題:以上例子中,這兩種Materialization的開銷分別是多少(從行讀和行寫的角度統計)

答案:

Materialize-lookup:40次讀student_class表,40次寫物化臨時表,42次讀外部表,40次lookup檢索物化臨時表;

Materialize-scan:15次讀student_class表,15次寫物化臨時表,15次掃描物化臨時表,執行15次class表索引查詢。

7.2.2、Materialization

優化器使用Materialization(物化)來實現更加有效的子查詢處理。物化針對非關聯子查詢進行優化。

物化通過把子查詢結果存儲為臨時表(通常在內存中)來加快查詢的執行速度。MySQL在第一次獲取子查詢結果時,會將結果物化為臨時表。隨後如果再次需要子查詢的結果,則直接從臨時表中讀取。

優化器可以使用哈希索引為臨時表建立索引,以使查找更加高效,並且通過索引來消除重複項,讓表保持更小。

子查詢物化的臨時表在可能的情況下存儲在內存中,如果表太大,則會退回到磁盤上進行存儲。

為何要使用物化優化

如果未開啟物化優化,那麼優化器有時會將非關聯子查詢重寫為關聯子查詢。

可以通過以下命令查詢優化開關(Switchable Optimizations[18])狀態:

SELECT @@optimizer_switch\G;

也就是說,如下的in獨立子查詢語句:

SELECT * FROM t1
WHERE t1.a IN (SELECT t2.b FROM t2 WHERE where_condition);

會重寫為exists關聯子查詢語句:

SELECT * FROM t1
WHERE EXISTS (SELECT t2.b FROM t2 WHERE where_condition AND t1.a=t2.b);

開啟了物化開關之後,獨立子查詢避免了這樣的重寫,使得子查詢只會查詢一次,而不是重寫為exists語句導致外部每一行記錄都會執行一次子查詢,嚴重降低了效率。

7.2.3、EXISTS策略

考慮以下的子查詢:

outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)

MySQL“從外到內”來評估查詢。也就是說,它首先獲取外部表達式outer_expr的值,然後運行子查詢並獲取其產生的結果集用於比較。

7.2.3.1、condition push down 條件下推

如果我們可以把outer_expr下推到子查詢中進行條件判斷,如下:

EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)

這樣就能夠減少子查詢的行數了。相比於直接用IN來說,這樣就可以加快SQL的執行效率了。

而涉及到NULL值的處理,相對就比較複雜,由於篇幅所限,這裏作為延伸學習,感興趣的朋友可以進一步閱讀:

8.2.2.3 Optimizing Subqueries with the EXISTS Strategy[19]

延伸:
除了讓關聯的in子查詢轉為exists進行優化之外。在MariaDB 10.0.2版本中,引入了另一種相反的優化措施:可以讓exists子查詢轉換為非關聯in子查詢,這樣就可以用上非關聯資產性的物化優化策略了。

詳細可以閱讀:EXISTS-to-IN Optimization[20]

7.2.4、總結

總結一下子查詢的優化方式:

  • 首先優先使用Semijoin來進行優化,消除子查詢,通常選用FirstMatch策略來做表連接;
  • 如果不可以使用Semijoin進行優化,並且當前子查詢是非關聯子查詢,則會物化子查詢,避免多次查詢,同時這一步的優化會遵循選用小表作為驅動表的原則,盡量走索引字段關聯,分為兩種執行方式:Materialize-lookup,Materialization-scan。通常會選用哈希索引為物化臨時表提高檢索效率;
  • 如果子查詢不能物化,那就只能考慮Exists優化策略了,通過condition push down把條件下推到exists子查詢中,減少子查詢的結果集,從而達到優化的目的。

8、limit offset, rows

limit的用法:

limit [offset], [rows]

其中 offset表示偏移量,rows表示需要返回的行數。

offset  limit  表中的剩餘數據
 _||_   __||__   __||__
|    | |      | |
RRRRRR RRRRRRRR RRR...
       |______|
          ||
         結果集

8.1、執行原理

MySQL進行表掃描,讀取到第 offset + rows條數據之後,丟棄前面offset條記錄,返回剩餘的rows條記錄。

比如以下sql:

select * from t30 order by id limit 10000, 10;

這樣總共會掃描10010條。

8.2、優化手段

如果查詢的offset很大,避免直接使用offset,而是通過id到聚集索引中檢索查找。

  1. 利用自增索引,如:
select * from t30 where id > 10000 limit 10;

當然,這也是會有問題的,如果id中間產生了非連續的記錄,這樣定位就不準確了。寫到這裏,篇幅有點長了,最後這個問題留給大家思考,感興趣的朋友可以進一步思考探討與延伸。

這篇文章的內容就差不多介紹到這裏了,能夠閱讀到這裏的朋友真的是很有耐心,為你點個贊。

本文為arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,如果你發現了有何錯漏之處,煩請高抬貴手幫忙指正,萬分感激。

大家可以關注我的博客:itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。

如果您覺得讀完本文有所收穫的話,可以關注我的賬號,或者點贊吧,碼字不易,您的支持就是我寫作的最大動力,再次感謝!

關注我的公眾號,及時獲取最新的文章。

更多文章

  • 關注公眾號進入會話窗口獲取
  • JVM系列專題:公眾號發送 JVM

本文作者: arthinking

博客鏈接: https://www.itzhai.com/database/how-sql-works-understand-the-essence-of-tuning-by-the-execution-principle.html

SQL運行內幕:從執行原理看調優的本質

版權聲明: BY-NC-SA許可協議:創作不易,如需轉載,請聯繫作者,謝謝!

References

  1. https://zhuanlan.zhihu.com/p/54378839. Retrieved from https://zhuanlan.zhihu.com/p/54378839 ↩︎

  2. 8.2.1.14 ORDER BY Optimization. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html ↩︎

  3. 8.8.2 EXPLAIN Output Format. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/explain-output.html ↩︎

  4. Batched Key Access: a Significant Speed-up for Join Queries. Retrieved from https://conferences.oreilly.com/mysql2008/public/schedule/detail/582 ↩︎

  5. Batched Key Access Joins. Retrieved from http://underpop.online.fr/m/mysql/manual/mysql-optimization-bka-optimization.html ↩︎

  6. [Hash join in MySQL 8. MySQL Server Blog. Retrieved from https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does](https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does) ↩︎

  7. MySQL JOINS Tutorial: INNER, OUTER, LEFT, RIGHT, CROSS. Retrieved from https://www.guru99.com/joins.html ↩︎

  8. How the SQL join actually works?. Retrieved from https://stackoverflow.com/questions/34149582/how-the-sql-join-actually-works ↩︎

  9. 13.2.9 SELECT Statement. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/select.html ↩︎

  10. 8.2.1.18 DISTINCT Optimization. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/distinct-optimization.html ↩︎ ↩︎

  11. Subquery Optimizer Hints. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html#optimizer-hints-subquery ↩︎

  12. 高性能MySQL第3版[M]. 电子工業出版社, 2013-5:239. ↩︎

  13. 8.2.2.1 Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/semijoins.html ↩︎

  14. FirstMatch Strategy. Retrieved from https://mariadb.com/kb/en/firstmatch-strategy/ ↩︎

  15. DuplicateWeedout Strategy. Retrieved from https://mariadb.com/kb/en/duplicateweedout-strategy/ ↩︎

  16. LooseScan Strategy. Retrieved from https://mariadb.com/kb/en/loosescan-strategy/ ↩︎

  17. Semi-join Materialization Strategy. Retrieved from https://mariadb.com/kb/en/semi-join-materialization-strategy/ ↩︎

  18. Switchable Optimizations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/switchable-optimizations.html ↩︎

  19. 8.2.2.3 Optimizing Subqueries with the EXISTS Strategy. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization-with-exists.html ↩︎

  20. EXISTS-to-IN Optimization. Retrieved from https://mariadb.com/kb/en/exists-to-in-optimization/ ↩︎

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

聯合國專家談循環經濟 勿落入回收迷思 「最難的是政治問題」

環境資訊中心記者 孫文臨報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

非洲南部陷旱災 農民牲畜受害慘

摘錄自2019年11月27日公視報導

非洲南部今年遇到了將近半個世紀以來最嚴重的乾旱,導致農民飼養的牲畜大量死亡,糧食也大量短缺,連人都吃不飽。難以維生的農民,因為債台高築、前途茫茫,已經有人走上絕路。

南非近來遭遇嚴重乾旱,小規模養殖的農民,正面臨牲畜不保、債台高築的困境。64歲的莫威,過去兩年來已經失去400頭綿羊,以及450隻提供打獵用的跳羚,他說:「我失去四分之一的綿羊,跳羚也是收入的一部分,我賣給獵人,他們會來打獵,我本來有450隻、500隻左右,現在找不到半隻,除了死掉的。」

莫威的聲音哽咽起來,他說幸好有教會幫忙,現在只能靠捐助的糧草來保住剩下的牲畜,但是已經有兩位農友因為壓力太大撐不下去,選擇輕生。

教會人員海門斯表示,「很多人現在都在掙扎著想輕生,包括這裡和西部地區,因為乾旱影響了南非廣大地區,也有人因為絕望結束自己的生命。」

乾旱衝擊農民生計,當地孩童也面臨飢餓危機,學校每天供應的玉米配蔬菜,就是窮苦孩子們早餐和午餐的全部。學校老師尼格薩蘇說,「這項供餐計畫對支持學生非常重要,尤其那些家裡沒錢的小孩。」

聯合國估計,乾旱加上連續兩個熱帶氣旋摧毀農田,非洲南部包括辛巴威和莫三比克地區,將有超過1100萬人出現糧荒危機。而氣象預報顯示,未來三個月當地降雨量仍將偏低。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

人類首次測到藍鯨心跳! 「最高每分鐘34下」解開世界最大體積動物之謎

摘錄自2019年11月27日ETtoday新聞雲報導

藍鯨是目前世界上體型最大的動物,也是地球史上已知動物中體型最大的,由於體型過大,使得科學家一直很難完整得知牠的生理特徵,近來卻有科學家首次測到藍鯨的心跳,也因此解開了更多關於藍鯨的祕密。

這項研究近來發表在《美國國家科學院院刊》(Proceedings of the National Academy of Sciences of the United States of America)上,一組海洋生物學家透過在藍鯨背部安裝吸盤的方式,成功在加州外海測到了心跳。研究人員長達九個小時持續觀察一條藍鯨發現,牠浮出水面時心跳最高可達每分鐘34下,沉入水面時心跳最低卻可降到每分鐘兩下。

科學家解釋,這是因為藍鯨潛入水中時,身體會重新分配氧氣,其心臟和大腦會需要比較多氧氣,肌肉、皮膚和其他器官所吸收的氧氣量較少,因此每呼吸一次便能更長時間停留在水中。

一條成年藍鯨身長可超過30公尺,讓科學家們一直很好奇,要有多大的力量才能為世界上體型最大的動物提供動力,2015年他們從藍鯨的解剖標本發現,其心臟竟然重達180公斤,看起來和一台高爾夫球車差不多大。

加利福尼亞大學助理教授戈德博根(Jeremy Goldbogen)表示,測得藍鯨的心跳能幫助他們了解,藍鯨為什麼沒有長得比現在更大,因為藍鯨身體所需要的動能要求,已經到達心臟所能承受的最大值,光是張開大口這個動作,就會讓其心臟運作達到極限。

若是地球上有體型比藍鯨更大的動物存在,其心臟的跳動速度會需要更快,但科學家認為從目前數據看起來這是不可能的事,也因此解釋了為何目前地球上沒有發現比藍鯨體積更大的動物存在之謎。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

日本女川核電廠2號反應爐獲准重新啟動 防波堤加高到29公尺

摘錄自2019年11月29日ETtoday報導

日本東北電力公司(Tohoku Electric Power)在27日表示,已經獲得日本核監管局的初步批准,重新啟動女川核電廠(Onagawa nuclear powerplant)2號反應爐。日本2011年大地震時,女川核電廠是最接近震央的一座核電廠,當時受到地震及海嘯的破壞而關閉,目前還需要當地居民的同意才能重新啟動。

女川核電廠在311地震時被海嘯淹沒,但裡面的冷卻系統倖存下來,使反應爐免遭受類似於東京電力福島第一核電廠發生的慘況。

東北電力公司預計為女川核電廠的安全升級投資3,400億日元(約新台幣950億元),將電廠防波堤的海拔高度提高到29公尺,這將會是日本各核電廠中最高的一座防波堤。若重新啟動2號反應爐,每年將為公用事業節省350億日元(約新台幣98億元)的燃料成本。

日本在福島核災之前有54座核反應爐在營運,提供日本三分之一的電力,災難突顯了營運和監管方面的缺陷之後,所有的核反應爐若要重啟,都必須按照新的標準許可。

日本的核能安全問題在最近才又被提起,天主教教宗方濟各在上周末訪問日本時表示,除非可以真正保障人民的安全,否則不該再使用核能。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

socketserver模塊使用與源碼分析

socketserver模塊使用與源碼分析

前言

  在前面的學習中我們其實已經可以通過socket模塊來建立我們的服務端,並且還介紹了關於TCP協議的粘包問題。但是還有一個非常大的問題就是我們所編寫的Server端是不支持併發性服務的,在我們之前的代碼中只能加入一個通信循環來進行排隊式的單窗口一對一服務。那麼這一篇文章將主要介紹如何使用socketserver模塊來建立具有併發性的Server端。

 

基於TCP協議的socketserver服務端

  我們先看它的一段代碼,對照代碼來看功能。

#!/usr/bin/env python3
# _*_ coding:utf-8 _*_

# ==== 使用socketserver創建支持多併發性的服務器 TCP協議 ====

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    """自定義類"""

    def handle(self):
        """handle處理請求"""
        print("雙向鏈接通道建立完成:", self.request)  # 對於TCP協議來說,self.request相當於雙向鏈接通道conn,即accept()的第一部分
        print("客戶端的信息是:", self.client_address)  # 對於TCP協議來說,相當於accept()的第二部分,即客戶端的ip+port

        while 1:  # 開始內層通信循環
            try:  # # bug修復:針對windows環境
                data = self.request.recv(1024)

                if not data:
                    break  # bug修復:針對類UNIX環境

                print("收到客戶機[{0}]的消息:[{1}]".format(self.client_address, data))
                self.request.sendall(data.upper())  # #sendall是重複調用send.

            except Exception as e:
                break

        self.request.close()  # 當出現異常情況下一定要關閉鏈接


if __name__ == '__main__':
    s1 = socketserver.ThreadingTCPServer(("0.0.0.0", 6666), MyServer) # 公網服務器綁定 0.0.0.0 私網測試為 127.0.0.1
    s1.serve_forever()  # 啟動服務

 

  1.導入socketserver模塊

  2.創建一個新的類,並繼承socketserver.BaseRequestHandler

  3.覆寫handle方法,對於TCP協議來說,self.request 相當於雙向鏈接通道connself.client_address相當於被服務方的ip和port信息,也就是addr,而整個handle方法相當於鏈接循環。

  4.寫入收發邏輯規則

  5.防止客戶端發送空的消息已致雙方卡死

  6.防止客戶端突然斷開已致服務端崩潰

  7.粘包優化(可選)

  8.實例化 socketserver.ThreadingTCPServer類,並傳入IP+port,以及剛寫好的類名

  9.使用socketserver.ThreadingTCPServer實例化對象中的server_forever( )方法啟動服務

 

  它其實是這樣的:

    我們不用管鏈接循環,因為在執行handle方法之前內部已經幫我們做好了。當我們使用serve_forever()方法的時候便開始監聽鏈接描述符對象,一旦有鏈接請求就創建一個子線程來處理該鏈接。

 

 

基於UDP協議的socketserver服務端

  基於UDP協議的socketserver服務端與基於TCP協議的socketserver服務端大相徑庭,但是還是有幾點不太一樣的地方。

 

  對TCP來說:

    self.request = 雙向鏈接通道(conn)

  對UDP來說:

    self.request = (client_data_byte,udp的套接字對象)

 

#!/usr/bin/env python3
# _*_ coding:utf-8 _*_

# ==== 使用socketserver創建支持多併發性的服務器 UDP協議 ====

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    """自定義類"""

    def handle(self):
        """handle處理請求"""

        # 由於UDP是基於消息的協議,故根本不用通信循環

        data = self.request[0]  # 對於UDP協議來說,self.request其實是個元組。第一個元素是消息內容主題(Bytes類型),相當於recvfrom()的第一部分
        server = self.request[1]    # 第二個元素是服務端本身,即自己

        print("客戶端的信息是:", self.client_address)   # 對於UDP協議來說,相當於recvfrom()的第二部分,即客戶端的ip+port

        print("收到客戶機[{0}]的消息:[{1}]".format(self.client_address, data))
        server.sendto(data.upper(),self.client_address)


if __name__ == '__main__':
    s1 = socketserver.ThreadingUDPServer(("0.0.0.0", 6666), MyServer)  # 公網服務器綁定 0.0.0.0 私網測試為 127.0.0.1
    s1.serve_forever()  # 啟動服務

 

擴展:socketserver源碼分析

探索socketserver中的繼承關係

  好了,接下來我們開始剖析socketserver模塊中的源碼部分。在Pycharm下使用CTRL+鼠標左鍵,可以進入源碼進行查看。

  我們在查看源碼前一定要首先要明白兩點:

 

  socketserver類分為兩部分,其一是server類主要是負責處理鏈接方面,另一類是request類主要負責處理通信方面。

 

  好了,請在腦子里記住這個概念。我們來看一些socketserver模塊的實現用了哪些其他的基礎模塊。

 

  注意,接下來的源碼註釋部分我並沒有在源代碼中修改,也請讀者不要修改源代碼的任何內容。

import socket  # 這模塊挺熟悉吧
import selectors  # 這個是一個多線程模塊,主要支持I/O多路復用。
import os # 老朋友了
import sys  # 老朋友
import threading  # 多線程模塊
from io import BufferedIOBase  # 讀寫相關的模塊
from time import monotonic as time  # 老朋友time模塊

socketserver中用到的基礎模塊

 

  好了,讓我們接着往下走。可以看到一個變量__all__,是不是覺得很熟悉?就是我們使用 from xxx import xxx 能導入進的東西全是被__all__控制的,我們看一下它包含了哪些內容。

__all__ = ["BaseServer", "TCPServer", "UDPServer",
           "ThreadingUDPServer", "ThreadingTCPServer",
           "BaseRequestHandler", "StreamRequestHandler",
           "DatagramRequestHandler", "ThreadingMixIn"]
           
# 這個是我們原本的 __all__ 中的值。
if hasattr(os, "fork"):
    __all__.extend(["ForkingUDPServer","ForkingTCPServer", "ForkingMixIn"])
if hasattr(socket, "AF_UNIX"):
    __all__.extend(["UnixStreamServer","UnixDatagramServer",
                    "ThreadingUnixStreamServer",
                    "ThreadingUnixDatagramServer"])
                    
# 上面兩個if判斷是給__all__添加內容的,os.fork()這個方法是創建一個新的進程,並且只在類UNIX平台下才有效,Windows平台下是無效的,所以這裏對於Windows平台來說就from socketserver import xxx 肯定少了三個類,這三個類的作用我們接下來會聊到。而關於socket中的AF_UNIX來說我們其實已經學習過了,是基於文件的socket家族。這在Windows上也是不支持的,只有在類UNIX平台下才有效。所以Windows平台下的導入又少了4個類。
​
​
# poll/select have the advantage of not requiring any extra file descriptor,
# contrarily to epoll/kqueue (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
    _ServerSelector = selectors.PollSelector
else:
    _ServerSelector = selectors.SelectSelector
    
# 這兩個if還是做I/O多路復用使用的,Windows平台下的結果是False,而類Unix平台下的該if結果為True,這關乎I/O多路復用的性能選擇。到底是select還是poll或者epoll。

socketserver模塊對於from xxx import * 導入的處理

 

  我們接着向下看源碼,會看到許許多多的類。先關掉它來假設自己是解釋器一行一行往下走會去執行那個部分。首先是一條if判斷

if hasattr(os, "fork"):
    class ForkingMixIn:
        pass # 這裏我自己省略了
 
 # 我們可以看見這條代碼是接下來執行的,它意思還是如果在類Unix環境下,則會去創建該類。如果在Windows平台下則不會創建該類

處理點一

 

  繼續走,其實這種if判斷再創建類的地方還有兩處。我這裏全部列出來:

if hasattr(os, "fork"):
    class ForkingUDPServer(ForkingMixIn, UDPServer): pass
    class ForkingTCPServer(ForkingMixIn, TCPServer): pass
 

if hasattr(socket, 'AF_UNIX'):
​
    class UnixStreamServer(TCPServer):
        address_family = socket.AF_UNIX
​
    class UnixDatagramServer(UDPServer):
        address_family = socket.AF_UNIX
​
    class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): passclass ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass

處理點二 and 三

 

  好了,說完了大體粗略的一個流程,我們該來研究這裏面的類都有什麼作用,這裏可以查看每個類的文檔信息。大致如下:

 

  前面已經說過,socketserver模塊中主要分為兩大類,我們就依照這個來進行劃分。

 

socketserver模塊源碼內部class功能一覽 
處理鏈接相關  
BaseServer 基礎鏈接類
TCPServer TCP協議類
UDPServer UDP協議類
UnixStreamServer 文件形式字節流類
UnixDatagramServer 文件形式數據報類
處理通信相關  
BaseRequestHandler 基礎請求處理類
StreamRequestHandler 字節流請求處理類
DatagramRequestHandler 數據報請求處理類
多線程相關  
ThreadingMixIn 線程方式
ThreadingUDPServer 多線程UDP協議服務類
ThreadingTCPServer 多線程TCP協議服務類
多進程相關  
ForkingMixIn 進程方式
ForkingUDPServer 多進程UDP協議服務類
ForkingTCPServer 多進程TCP協議服務類

 

  他們的繼承關係如下:

ForkingUDPServer(ForkingMixIn, UDPServer)
​
ForkingTCPServer(ForkingMixIn, TCPServer)
​
ThreadingUDPServer(ThreadingMixIn, UDPServer)
​
ThreadingTCPServer(ThreadingMixIn, TCPServer)
​
StreamRequestHandler(BaseRequestHandler)
​
DatagramRequestHandler(BaseRequestHandler)

 

  處理鏈接相關

處理通信相關

多線程相關

 

總繼承關係(處理通信相關的不在其中,並且不包含多進程)

 

  最後補上一個多進程的繼承關係,就不放在總繼承關係中了,容易圖形造成混亂。

 

多進程相關

 

實例化過程分析

  有了繼承關係我們可以來模擬實例化的過程,我們以TCP協議為準:

 

socketserver.ThreadingTCPServer(("0.0.0.0", 6666), MyServer)

 

  我們點進(選中上面代碼的ThradingTCPServer部分,CTRL+鼠標左鍵)源碼部分,查找其 __init__ 方法:

class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

 

  看來沒有,那麼就找第一父類有沒有,我們點進去可以看到第一父類ThreadingMixIn也沒有__init__方法,看上面的繼承關係圖可以看出是普通多繼承,那麼就是廣度優先的查找順序。我們來看第二父類TCPServer中有沒有,看來第二父類中是有__init__方法的,我們詳細來看。

class TCPServer(BaseServer):

   """註釋全被我刪了,影響視線"""

    address_family = socket.AF_INET  #  基於網絡的套接字家族

    socket_type = socket.SOCK_STREAM  # TCP(字節流)協議

    request_queue_size = 5  # 消息隊列最大為5,可以理解為backlog,即半鏈接池的大小

    allow_reuse_address = False  # 端口重用默認關閉

    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
        """Constructor.  May be extended, do not override."""
        BaseServer.__init__(self, server_address, RequestHandlerClass)
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
                                    
       # 可以看見,上面先是調用了父類的__init__方法,然後又實例化出了一個socket對象!所以我們先不着急往下看,先看其父類中的__init__方法。
       
        if bind_and_activate:
            try:
                self.server_bind()
                self.server_activate()
            except:
                self.server_close()
                raise

TCPServer中的__init__()

 

  來看一下,BaseServer類中的__init__方法。

class BaseServer:

    """註釋依舊全被我刪了"""

    timeout = None  # 這個變量可以理解為超時時間,先不着急說他。先看 __init__ 方法

    def __init__(self, server_address, RequestHandlerClass):
        """Constructor.  May be extended, do not override."""
        self.server_address = server_address  # 即我們傳入的 ip+port ("0.0.0.0", 6666)
        self.RequestHandlerClass = RequestHandlerClass  # 即我們傳入的自定義類 MyServer
        self.__is_shut_down = threading.Event()  # 這裏可以看到執行了該方法,這裏先不詳解,因為它是一個事件鎖,所以不用管
        self.__shutdown_request = False 

BaseServer中的__init__()

 

  BaseServer中執行了thrading模塊下的Event()方法。我這裏還是提一嘴這個方法是幹嘛用的,它會去控制線程的啟動順序,這裏實例化出的self.__is_shut_down其實就是一把鎖,沒什麼深究的,接下來的文章中我也會寫到。我們繼續往下看,現在是該回到TCPServer__init__方法中來了。

class TCPServer(BaseServer):

   """註釋全被我刪了,影響視線"""
   
    address_family = socket.AF_INET  #  基於網絡的套接字家族

    socket_type = socket.SOCK_STREAM  # TCP(字節流)協議

    request_queue_size = 5  # 消息隊列最大為5,可以理解為backlog,即半鏈接池的大小
    
    allow_reuse_address = False  # 端口重用默認關閉

    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True): # 看這裏!!!!
    """Constructor.  May be extended, do not override."""
        BaseServer.__init__(self, server_address, RequestHandlerClass)
        self.socket = socket.socket(self.address_family,
                                self.socket_type)                        
   
        if bind_and_activate:  # 在創建完socket對象后就會進行該判斷。默認參數bind_and_activate就是為True
            try:
                self.server_bind() # 現在進入該方法查看細節
                self.server_activate()
            except:
                self.server_close()
                raise

TCPServer中的__init__()


  好了,需要找這個self.bind()方法,還是從頭開始找。實例本身沒有,第一父類ThreadingMixIn也沒有,所以現在我們看的是TCPServerserver_bind()方法:

def server_bind(self):
    """Called by constructor to bind the socket.

    May be overridden.

    """
    if self.allow_reuse_address:  # 這裏的變量對應 TCPServer.__init__ 上面定義的類方法,端口重用這個。由於是False,所以我們直接往下執行。
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self.socket.bind(self.server_address)  # 綁定 ip+port 即 ("0.0.0.0", 6666)
    self.server_address = self.socket.getsockname() # 獲取socket的名字 其實還是 ("0.0.0.0", 6666)

TCPServer中的server_bind()

 

  現在我們該看TCPServer下的server_activate()方法了。

def server_activate(self):
    """Called by constructor to activate the server.

    May be overridden.

    """
    self.socket.listen(self.request_queue_size)  # 其實就是監聽半鏈接池,backlog為5

TCPServer中的server_activate()

 

  這個時候沒有任何異常會拋出的,所以我們已經跑完了整個實例化的流程。並將其賦值給s1

  現在我們看一下s1__dict__字典,再接着進行源碼分析。

{'server_address': ('0.0.0.0', 6666), 'RequestHandlerClass': <class '__main__.MyServer'>, '_BaseServer__is_shut_down': <threading.Event object at 0x000002A96A0208E0>, '_BaseServer__shutdown_request': False, 'socket': <socket.socket fd=716, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 6666)>}

s1的__dict__

 

server_forever()啟動服務分析

  我們接着來看下一條代碼。

s1.serve_forever()

 

  還是老規矩,由於s1ThreadingTCPServer類的實例對象,所以我們去一層層的找serve_forever(),最後在BaseServer類中找到了。

def serve_forever(self, poll_interval=0.5):
    """註釋被我刪了"""
    self.__is_shut_down.clear()  # 上面說過了那個Event鎖,控制子線程的啟動順序。這裏的clear()代表清除,這個不是重點,往下看。
    try:
        # XXX: Consider using another file descriptor or connecting to the
        # socket to wake this up instead of polling. Polling reduces our
        # responsiveness to a shutdown request and wastes cpu at all other
        # times.
        with _ServerSelector() as selector:  
            selector.register(self, selectors.EVENT_READ)# 這裡是設置了一個監聽類型為讀取事件。也就是說當有請求來的時候當前socket對象就會發生反應。

            while not self.__shutdown_request: # 為False,會執行,注意!下面都是死循環了!!!
                ready = selector.select(poll_interval)  # 設置最大監聽時間為0.5s
                # bpo-35017: shutdown() called during select(), exit immediately.
                if self.__shutdown_request: # BaseServer類中的類方法,為False,所以不執行這個。
                    break
                if ready: # 代表有鏈接請求會執行下面的方法
                    self._handle_request_noblock()  # 這兒是比較重要的一個點。我們先來看。

                self.service_actions() 
    finally:
        self.__shutdown_request = False
        self.__is_shut_down.set() # 這裡是一個釋放鎖的行為

BaseServer中的serve_forever()

 

  如果有鏈接請求,則會執行self._handle_request_noblock()方法,它在哪裡呢?剛好這個方法就在BaseServerserve_forever()方法的正下方第4個方法的位置。

def _handle_request_noblock(self):
    """註釋被我刪了"""
    try:
        request, client_address = self.get_request()  # 這裏的這個方法在TCPServer中,它的return值是 self.socket.accept(),就是就是返回了元組然後被解壓賦值了。其實到這一步三次握手監聽已經開啟了。
    except OSError:
        return
    if self.verify_request(request, client_address): # 這個是驗證ip和port,返回的始終是True
        try:
            self.process_request(request, client_address) # request 雙向鏈接通道,client_address客戶端ip+port。現在我們來找這個方法。
        except Exception:
            self.handle_error(request, client_address)
            self.shutdown_request(request)
        except:
            self.shutdown_request(request)
            raise
    else:
        self.shutdown_request(request)

BaseServer中的_handle_request_noblock()

 

  現在開始查找self.process_request(request, client_address)該方法,還是先從實例對象本身找,找不到去第一父類找。他位於第一父類ThreadingMixIn中。

def process_request(self, request, client_address):
    """Start a new thread to process the request."""
    t = threading.Thread(target = self.process_request_thread,
                         args = (request, client_address))  # 創建子線程!!看這裏!
    t.daemon = self.daemon_threads # ThreadingMixIn的類屬性,為False
    if not t.daemon and self.block_on_close:  # 第一個值為False,第二個值為True。他們都是ThreadingMixIn的類屬性
        if self._threads is None:  # 會執行
            self._threads = []  # 創建了空列表
        self._threads.append(t) # 將當前的子線程添加至空列表中
    t.start()  # 開始當前子線程的運行,即運行self.process_request_thread方法

ThreadingMixIn中的process_request()

 

  我們可以看到,這裏的target參數中指定了一個方法self.process_request_thread,其實意思就是說當這個線程tstart的時候會去執行該方法。我們看一下它都做了什麼,這個方法還是在ThreadingMixIn類中。

def process_request_thread(self, request, client_address):
    """Same as in BaseServer but as a thread.

    In addition, exception handling is done here.

    """
    try:
        self.finish_request(request, client_address) # 可以看到又執行該方法了,這裏我再標註一下,別弄頭暈了。request 雙向鏈接通道,client_address客戶端ip+port。
    except Exception:
        self.handle_error(request, client_address)
    finally:
        self.shutdown_request(request)  # 它不會關閉這個線程,而是將其設置為wait()狀態。

ThreadingMixIn中的 process_request_thread()

 

  self.finish_request()方法,它在BaseServer類中

def finish_request(self, request, client_address):
    """Finish one request by instantiating RequestHandlerClass."""
    self.RequestHandlerClass(request, client_address, self)  # 這裡是幹嘛?其實就是在進行實例化!

BaseServer中的finish_request


  self.RequestHandlerClass(request, client_address, self),我們找到self__dict__字典,看看這個到底是什麼東西

{'server_address': ('0.0.0.0', 6666), 'RequestHandlerClass': <class '__main__.MyServer'>, '_BaseServer__is_shut_down': <threading.Event object at 0x000002A96A0208E0>, '_BaseServer__shutdown_request': False, 'socket': <socket.socket fd=716, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 6666)>}

s1的__dict__

 

  可以看到,它就是我們傳入的那個類,即自定義的MyServer類。我們把request,client_address,以及整個是實例self傳給了MyServer的__init__方法。但是我們的MyServer類沒有__init__,怎麼辦呢?去它父類BaseRequestHandler裏面找唄。

class BaseRequestHandler:

    """註釋被我刪了"""

    def __init__(self, request, client_address, server):
        self.request = request  # request 雙向鏈接通道
        self.client_address = client_address  # 客戶端ip+port
        self.server = server # 即 實例對象本身。上面的__dict__就是它的__dict__
        self.setup() # 鈎子函數,我們可以自己寫一個類然後繼承`BaseRequestHandler`並覆寫其setup方法即可。
        try:
            self.handle()  # 看,自動執行handle
        finally:
            self.finish()  # 鈎子函數

    def setup(self):
        pass

    def handle(self):
        pass

    def finish(self):
        pass

BaseRequestHandler中的__init__

 

 

  現在我們知道了,為什麼一定要覆寫handle方法了吧。

 

socketserver內部調用順序流程圖(基於TCP協議)

 

實例化過程圖解

 

server_forever()啟動服務圖解

 

擴展:驗證鏈接合法性

  在很多時候,我們的TCP服務端為了防止網絡泛洪可以設置一個三次握手驗證機制。那麼這個驗證機制的實現其實也是非常簡單的,我們的思路在於進入通信循環之前,客戶端和服務端先走一次鏈接認證,只有通過認證的客戶端才能夠繼續和服務端進行鏈接。

  下面就來看一下具體的實現步驟。

 

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    認證客戶端鏈接
    :param conn:
    :return:
    '''
    print('開始驗證新鏈接的合法性')
    msg=os.urandom(32)  # 新方法,生成32位隨機Bytes類型的值
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest) # 對比結果為True或者為False

def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('該鏈接不合法,關閉')
        conn.close()
        return
    print('鏈接合法,開始通信')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,bufsize,backlog=5):
    '''
    只處理鏈接
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新連接[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)

Server端

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    驗證客戶端到服務器的鏈接
    :param conn:
    :return:
    '''
    msg=conn.recv(32) # 拿到隨機位數
    h=hmac.new(secret_key,msg) # 摻鹽
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)

Client端

 

  到這裏已經很簡單了,服務器將隨機數給客戶機發過去,客戶機收到后也用自家的鹽與隨機數加料,再使用digest()將它轉化為字節,直接發送了回來然後客戶端通過hmac.compare_digest()方法驗證兩個的值是否相等,如果不等就說明鹽不對。客戶機不合法服務端將會關閉與該客戶機的雙向鏈接通道。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

讀懂操作系統之虛擬內存頁表(五)

前言

在一個擁有32位的地址空間,4KB的頁面(212),並且每個PTE為4個字節,那麼頁表大小為4MB(4 * 232 / 212),但若為64位地址空間,4KB的頁面(212)且每個PTE為4字節,那麼頁表大小為16TB(4 * 264 / 212),由於頁表常駐內存,佔用內存會很大,所以必須對頁表存儲結構進行優化,這就是我們本文所要講解的內容,常見的頁表數據結構為多級頁表(兩級、三級等)、倒置頁表、哈希頁表,我們來一一進行分析。

多級頁表

首先我們講講2級頁表,然後通過2級頁表延伸到多級頁表,現假設有16KB(214)的地址空間,並且每頁大小為64(26)字節,每個PTE為4字節,那麼說明頁表為1KB(4 * 214 / 26),若我們有64字節的頁,那麼將1KB可劃分為16個64字節的頁面,每個頁面可容納16個PTE,前面我們講解到虛擬地址劃分為虛擬頁號(VPN)和虛擬頁偏移量(VPO),但虛擬頁偏移量已經固定,那麼我們只能從VPN下手將其作為索引用於索引頁表目錄,那麼我們該如何利用VPN來構建各個部分的索引呢?我們首先需要構建頁表目錄,根據上述假設,總共有256個PTE分佈在16頁上,頁目錄在頁表的每頁上需要一個條目,因此它具有16個條目, 最終我們需要VPN中的4位索引到目錄中,這也就意味着我們需要使用VPN的前4位,如下所示:

我們從VPN提取出了頁表目錄索引(PDI),那麼我們就可以計算出每個PDE(Page Directory Entry)的地址值:

PDEAddress = PageDirectoryBase + (PDIndex * sizeof(PDE))。

有了頁目錄索引后我們還需進行進一步翻譯,如果頁目錄索引為空,很顯然第2級頁表根本就不會存在,如此一來則達到了減少內存的要求,因為只有第一級頁表才會存在於主存中,虛擬內存系統會根據需要調入或調出第2級頁表,這就減少了主存的壓力,只有經常使用的2級頁表才需要緩存在主存中。如果第1級頁表即頁目錄索引有值,那麼還需根據頁目錄指向的頁表頁面去獲取PTE,要找到此PTE,我們還需要使用VPN的其餘索引映射到頁表的部分。

 通過使用如上頁表索引來索引頁表本身,從而找到PTE地址,也就找到了PFN(物理頁幀號)

PTEAddress = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))

從頁面目錄獲得的頁面幀號(PFN)必須先左移到適當位置,然後再與頁表索引組合以形成PTE的地址。假設如下為二級頁表扁平化的片段

 

如上第1片段為頁表目錄,在其中存在索引到第2級頁表的索引,還包括有效位,第2和第3片段分別為第1級頁表目錄索引對應的頁表(其中包含保護位,可讀?可寫?等等),假設CPU產生虛擬地址(0xFE16 = 25410 = 111111102),由於我們假設虛擬地址空間為14位,所以將轉換后的2進制不足用0填充即11111110000000,同時我們將地址空間進行虛擬頁號(VPN)和虛擬頁偏移量(VPO)劃分,然後對VPN劃分為頁表目錄和頁表索引,我們通過紅色、綠色、藍色由左至右分別代表頁表目錄索引、頁表索引、虛擬頁偏移量即1111 1110  000000,經過如此劃分后,此時前4位(11112 = 1510)為頁表目錄索引,對應上述頁表目錄最後一行,此時頁表目錄幀號為101對應第2個頁表片段,然後根據接下來的4位(11102 = 1410),最終得到索引為倒數第2行,即最終物理頁幀號為55。最後我們通過如下物理地址計算公式

 PhysAddress = (PTE.PFN << SHIFT) + offset

 即最終物理地址為:55 * 2+ 000000 = 352010 = 0XDC016。假設為32位地址空間,那麼頁目錄索引、頁表索引、虛擬頁偏移量分別對應為10、10、12位,那麼對應的2級頁表將是如下形式

簡而言之,對於32位地址空間,會將VPN中的前10位(位22..31)用於索引頁表目錄,緊接下來的10位(12 ..21)用於索引所選的頁表。換言之,對於2級頁表結構其本質是:VPN的前m位為頁表目錄索引,而接下來的n位為頁表索引,同時需要注意的是2級頁表其地址是從上往下增加。根據上述將32位地址空間中的頁表以2級結構劃分,此時第1級頁表大小為(1024 * 4) = 4KB,而第2級頁表為(1024 * 1024 * 4) = 4MB,所以頁表大小將為4KB + 4MB,這麼算來比直接使用單級頁表結構為4MB情況更糟糕了不是嗎,其實情況並不是這樣,如上算出的4KB + 4MB為最極限的情況,上述已經講解過只有經常需要用到的2級頁表才緩存在主存中,所以實際情況下頁表大小會小於4MB。

 

早期操作系統採用的是2級頁表結構,但是現如今大多數操作系統採用多級頁表結構,就像樹一樣,不過是深度或層次更深了而已。假設我們有一個30位虛擬地址空間和一個較小的頁面(512字節),因此,我們的虛擬地址具有21位的虛擬頁號和9位的偏移量,使頁表的每個部分都適合單個頁面是構建多級頁表的目標,但到目前為止,我們僅考慮了頁表本身,如果頁表目錄很大,那該怎麼辦?為了確定一個多級頁表中需要多少級才能使頁表的所有部分用一個頁面容納,我們首先確定一個頁面中可以容納多少個PTE。我們假設給定的頁面大小為512字節,並假設PTE大小為4字節,我們知道在單個頁面上可容納128個PTE。當我們索引到頁表的頁面時,可以得出結論,我們需要使用VPN的最低有效7位(log2128)作為索引

通過確定單頁面需要容納128個PTE,那麼將佔據地址空間7位,那麼還剩下14位地址空間,如果將剩下的214作為頁表目錄, 那麼將橫跨128頁而不再是1頁,那麼對於構建多級頁表的目標將無法實現,為了解決這個問題,我們需要將14位進行再次劃分,將頁表目錄進行設置為多頁,頁表目錄位於上方從而指向另一頁表目錄,因此我們可以進行如下劃分

現在,在索引上層頁表目錄時,我們使用虛擬地址的最高位(圖中PD Index:0),該索引可用於從頂級頁表目錄中獲取頁表目錄的條目,如果有效,則對來自自頂層PDE和VPN的下一部分(PD Index:1)的物理幀號組合來查詢頁表目錄的第二層,最後,如果有效,則為PTE地址通過將頁表索引與第二級PDE中的地址結合使用,可以形成一個地址。 當然這個過程需要做很多工作,所有這些都是為了在多級表中查找物理頁幀號。最終多級頁表結構如下這般

上述我們講過若為64位地址空間,4KB的頁面(212)且每個PTE為4字節,在單級頁表情況下,那麼頁表大小為16TB(4 * 264 / 212)= 16TB,若我們劃分為3級,如下:

 

那麼對於上述外部頁即頁目錄索引將需要佔內存4 * 232 = 16GB,所以我們仍需繼續劃分層級,但是每個層級都有一個額外的間接方式,因此會產生額外的開銷。比如64位地址空間在4KB頁面上將使用大地址空間,所以多級頁表成為具有小頁的大地址空間的內存消耗。 

哈希頁表

處理大於32位地址空間常用的方法是使用哈希頁表(使用稀疏的地址空間),採用虛擬頁碼作為哈希值,對於每一個PTE使用鏈表結構存儲從而解決衝突或碰撞,每個元素由三個字段組成:虛擬頁碼、映射的頁幀、指向鏈表內下一個元素的指針。通過哈希算法將虛擬頁碼映射到哈希頁表,然後將虛擬頁碼與鏈表第一個元素的第一個字段進行比較,若匹配則將第二個字段用來形成物理地址,否則遍歷鏈表查找對應匹配項。哈希頁表如下圖所示

雖然通過哈希頁表查找很快,同時採用如上划重點標記的鏈表數據結構解決衝突問題,雖說消除了條目在內存中連續的需求,但是仍然以更高的內存開銷進行存儲即消耗更多內存,特別是如果頁表是完整的,並且具有有效/無效位以使未使用的條目無效,那麼哈希頁表不再那麼適用,此時我們採用其他方案,如下倒置頁表。

倒置頁表

通過前面內容學習我們知道對於每個進程都有一個關聯的頁表,該進程中的每一個虛擬頁都在頁表中對應一項,不管是否有效,進程通過虛擬地址引用頁,操作系統通過計算虛擬地址在頁表中的位置即PTE,但這種方式有明顯的缺點,如上我們也敘述過,每個頁表可能包含數以百萬計的條目,如此一來,頁表將佔用大量的物理內存以跟蹤其他物理內存是如何使用的,為解決這個問題,我們可以使用倒置頁表(inverted page table),對於每個真正的內存頁,倒置頁表才有一個條目,每個條目包含保存在真正內存位置上的頁的虛擬地址,以及擁有該頁進程的信息,因此,整個系統中所有進程將只有一個頁表,並且每個物理內存的頁只有一個相應的條目,換言之,與知道每個進程的虛擬頁在哪裡相反,現在我們知道擁有哪個物理頁的進程與它對應的虛擬頁。IBM是最早採用倒置頁表的公司,從IBM System 38、RS/6000、到現代的IBM Power CPU。對於IBM  RT,系統的虛擬地址包含三部分:進程Id、頁碼、頁偏移量,每個倒置頁表條目包含兩部分:進程Id、頁碼,這裏的進程Id作為地址空間的標識符,當發生內存引用時,由進程Id和頁碼組成的虛擬地址被提交到內存子系統,然後搜索倒置頁表來尋址匹配,如果找到匹配條目,則生成物理地址,如果未找到匹配條目則為非法地址訪問。 倒置頁表結構如下:

雖然倒置頁表減少了存儲每個頁表所需的內存空間,但是它增加了由於引用頁而查找頁表所需要的時間,由於倒置頁表是按照物理地址排序,而查找則是根據虛擬地址,因此查找匹配可能需要搜索整個表,這種搜索需要耗費很長時間,為解決這個問題,可以使用一個哈希表結構,從而將搜索限制在一個或最多數個頁表條目,當然,每訪問哈希表就增加了一次內存引用,因此每次虛擬地址的引用至少需要兩個內存讀,一個用於哈希表條目,另一個用於頁表條目即PTE,同時結合前面所學,在搜索哈希表之前,肯定先搜索TLB,這樣可大大提高性能。對於倒置頁表還會帶來一個問題,那就是實現共享內存,共享內存需要將多個虛擬地址映射到同一物理地址,很顯然,這種標準的方式無法應用於倒置頁表,因為每一個物理頁只有一個虛擬頁條目,一個物理頁不可能有兩個或多個共享的虛擬地址,所以為解決這個問題,只能允許頁表包含一個虛擬地址到共享物理地址的映射,這也就意味着,對於未映射的虛擬地址的引用勢必會導致頁錯誤。

總結

本節我們非常詳細的討論了多級頁表結構、對於哈希頁表和倒置頁表數據結構通過看圖理解起來非常簡單,從本節內容我們可總結出:對應頁表結構可以擁有良好的時間複雜度或空間複雜度,但不能同時兼得。到此關於虛擬內存重要內容基本上都已囊括,若有遺漏,後續我會繼續進行補充。接下來我們將進入內存管理分頁和分段的學習,講完之後,會陸續進入到程序的執行、進程、死鎖、併發等,相信大家會比較感興趣,感謝您的閱讀,我們下節再見。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

人象共用44%國土 斯里蘭卡7隻大象遭毒殺 衝突待解

環境資訊中心外電;范震華 翻譯;賴慧玲 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

控制暖化不超過2°C 燃煤電廠需大規模改建更新 | 解讀《 2019年世界能源展望》報告1/3

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:Carbon Brief

國際能源署(IEA)13日發表報告。報告中指出,全球能源系統正在進行「深層更新」,但儘管如此,除非積極度再提升,否則全球的二氧化碳排放量還是會繼續增長數十年。

今年810頁版本的特點在於「承諾政策情境(Stated Policies Scenario, STEPS)」(以前稱為「新政策情境」),反映政府已經說出口的政策的效果。風能和太陽能的激增將使再生能源滿足全球能源需求的大部分成長。但是煤炭的平穩發展,加上對石油和天然氣的需求不斷增加,全球排放量在到2040年的展望期內將繼續上升。

相對地,報告的「永續發展情境(Sustainable Development Scenario, SDS)」描繪出有50%機率將升溫限制在1.65°C內所需的條件,IEA表示這是「完全符合巴黎協定」的情況。

報告說,SDS需要投資「大量重新分配」,從化石燃料轉向效率和再生能源、淘汰全球約一半的燃煤電廠,以及全球經濟的其他變化。

IEA今年還探討了將升溫限制在工業化前1.5°C內(即巴黎協定的理想目標)所需條件,不過沒有建立詳細模型。

未來情境

《世界能源展望》(WEO)是這個主題的年度出版物中審查最嚴格的報告之一。長達數百頁的分析以世界各國政府的數千個資料庫和IEA的世界能源模型為基礎。

IEA表示報告沒有對前景預測。相反地,它以二氧化碳排放量和其他資料形式呈現了特定能源選擇的後果。該報告說明道:

「《世界能源展望》的目的不是提供一個關於2030年或2040年世界能源將在何處的觀點。這將取決於未來的各種重大選擇。 WEO-2019的目的是,提供決策者制訂新政策、考慮新投資或以其他方式塑造能源未來時所需的資訊。透過探索各種可能的未來、實現的方式、不同選擇的後果以及一些關鍵的不確定性來做到這一點。」

引言中介紹了三種可能的「前景」,在報告發表前,部落格文章「了解WEO情境」也提前對此做了說明。

展望報告的中心觀點是「STEPS」,它的目的是「在不預期未來有所改變的情況下,讓政策制定者好好審視自己的計畫和積極度會有何表現」,包括政府做出的巴黎氣候承諾。不過,IEA並不認為所有政策目標都會實現:

「積極度不會自動被納入情境中:徹底落實不是理所當然的,因此政策落實的前景和時機是以我們對相關監管、市場、基礎建設和財務限制的評估為基礎。」

承諾政策包括零排放淨目標,如英國的目標。 IEA表示,已商定或討論中的類似目標(包括歐盟)涵蓋了全球排放量的12%。就解決全球排放而言,這些目標很重要,但不是決定性的。IEA表示,為實現零排放目標而開發的技術和方法可能會產生更大的連鎖反應,這可能有助於其他國家減少排放。

WEO的第二個未來情境是「永續發展情境(SDS)」。這個情境從能源獲取、空氣污染和碳排放方面的永續發展目標開始,探討達到目標所需的條件。

最後,在「當前政策情境(Current Policies Scenario, CPS)」中,政府放棄其承諾目標和意圖,能源系統僅以已經制定好的政策和法律為指導。

今年的報告文字、圖表和數據將繼續提到CPS,只是重要性較低,「當前政策情境」這個詞在810頁中僅使用了102次,遠少於STEPS的793次和SDS的535次。報告也說明,CPS就是不採取行動的後果,可看出STEPS情境所需的額外努力。

(2010年《世界經濟展望》 中,CPS被提及了340次,而當時的中心觀點NPS出現981次,「450種情境」出現745次。)

需求上升

IEA表示,根據全球已承諾的計畫和政策,到2040年,全球能源需求將繼續每年增長1%,相當於中國目前的總需求量。

根據國際貨幣基金組織(IMF)的預測,人口增加(根據聯合國的「中等」預測,2040年將達到90億人口)和經濟持續擴大(全球GDP每年增加3.4%)推動了這個成長。

IEA表示,由於產業轉向低能耗,能源效率的提高和「飽和效應」(如汽車需求封頂),2019年能源需求的成長率大約是2000年以來平均2%的一半。

如下圖中的紅線所示,STEPS中再生能源將滿足約49%的需求增長。天然氣的使用也預期將迅速增加(藍色),超過煤炭成為僅次於石油的第二大能源,並滿足總體需求增長的三分之一。

1990年至2040年全球各類型能源需求量,單位是數百萬噸石油當量。未來需求預測以STEPS為基礎。其他再生能源包括太陽能、風能、地熱能和海洋能。資料來源:國際能源署《 2019年世界能源展望》。Carbon Brief使用Highcharts繪製圖表。

與天然氣和再生能源的快速發展相比,IEA STEPS情境預測煤炭使用量將達到平穩,然後從今日的水平略降(上方黑線)。這呼應去年的分析,即全球煤炭需求在2014年達到頂峰。

IEA現在還建議,由於汽車燃油效率的提高和電動汽車(EV)的增加,到2030年(橙色線)石油需求將開始趨於穩定,這將使汽車的石油需求在2020年代末期達到峰值。報告說,由於電動汽車成本下降,傳統汽車的未來將是值得深思的問題。

IEA表示,貨運、航運、航空和化學產品的石油需求「持續增長」,SUV因日益普及,成為另一個潛在的需求支撐因素。(值得注意的是,阿拉伯國家石油公司的股票銷售文件也顯示,全球石油需求將從2035年左右開始趨於穩定。)

根據IEA STEPS,到2040年,全球能源需求增長的三分之二來自亞太地區。印度成為世界上人口最多的國家,其能源需求成長一倍,成為全球需求成長的最大貢獻者,佔成長總量的四分之一以上。

在這一總數中,STEPS預測亞洲國家對煤炭的需求增加抵消了美國和歐洲的大幅減少。IEA說:

「煤炭需求來自大多數亞洲發展中國家:在煤基礎設施方面的新投資決策已顯著放緩,但是現有的煤電廠和用煤工廠還是很多……為煤炭提供了可觀的發展動力。」

比例變動

STEPS之下,到2040年,再生能源的興起體現了IEA所形容的「深層更新」,但同時也指出了全球能源系統「變動緩慢」的特性,如煤炭長期的需求高原。

需求成長的比例變化顯示在下面圖表中,煤炭、石油和天然氣(藍色色塊)滿足了能源史上的大部分成長(最左欄)。

儘管STEPS之下,再生能源能滿足2040年需求成長一半,而且成長速度因為經濟因素和能源效率變化而放慢(中間欄),但它仍然遠遠沒有限制全球碳排放量(參閱下文)。

如果要阻止全球氣溫上升,需要有更具決定性的變化,如實現IEA SDS(最右欄)。

全球能源需求年均變化量,以百萬噸石油當量為單位。左:歷史變化。中:IEA STEPS。右:IEA SDS。資料來源:國際能源署《 2019年世界能源展望》 。Carbon Brief使用Highcharts繪製。

在STEPS之下可見再生能源所能滿足的需求成長越來越多,化石燃料在全球能源用量的比例將從2018年的81%下降到2040年的74%,SDS之下則下降到58%。

從STEPS到SDS的需要大規模的變革,其中大多數已經在決策者的議程中很久了。報告解釋:

「2018年化石燃料消費補貼的全球價值,幾乎是再生能源和電動汽車補貼以及全球碳定價計畫收入總和的兩倍。這種不平衡使排放儘早達到峰值的任務變得十分複雜。」

SDS之下,2030年代無碳捕集的化石燃料投資將減少至2014年到2018年平均的一半,再生能源、電網和核能方面的投資將翻倍,而在能源效率上的支出將翻兩倍。

IEA表示,這反映了一個事實,即能源效率是解決排放的最重要因素,這表示SDS下2040年的總體需求會略低於今日的水平。

IEA說「提高能源效率很有機會讓全世界實現永續能源目標」,它召集了「全球能源效率緊急行動委員會」來促成進展。

某種程度上這是對數據的回應,數據顯示效率的改善正在放緩,2018年的效率成長率是2010年以來最低的,這「疲軟的氣力」直得「深切關注」。IEA表示「新能源效率政策和加強現有措施的努力相對缺乏」。

較低的需求會帶來連鎖反應,特別是加上再生能源的快速成長。值得注意的是,在SDS之下,對煤炭、石油和天然氣的需求逐漸下降,而煤炭減幅特別大(上方最右欄中的灰色部分)。

在這個總數中,IEA說,電業的煤炭用量受影響最大。到2040年,超過一半的當前燃煤電廠將退役,規模大於全中國目前的容量。

有半數的退役發生在壽命結束之前,如果將升溫保持在2°C以下,那麼投資於全球現有煤電廠的10億美元中,有部分將面臨風險。歐洲222GW燃煤中有約98%、美國276GW中約88%將關閉。

IEA說,在SDS之下,剩餘的燃煤電廠大部分必須「改建或翻新」。他們得在需求高峰和再生能源產出低谷期間運作有限的時間,不然就得大量投資碳捕集與封存(CCS)技術來減排。

今年的展望報告包含了對煤炭開採過程中釋放的甲烷的新分析。分析結果說明,與航空和航運業相比,煤炭開採的暖化效應更大。(1/3,)

‘Profound shifts’ underway in energy system, says IEA World Energy Outlook (1/3) by Simon Evans

The world’s CO2 emissions are set to continue rising for decades unless there is greater ambition on climate change, despite the “profound shifts” already underway in the global energy system.

That is one of the key messages from the International Energy Agency’s (IEA) , published today. This year’s 810-page edition is notable for its renamed central “Stated Policies Scenario” (STEPS), formerly known as the “New Policies Scenario”.

In this scenario, which aims to mirror the outcome of policies already set out by governments, a surge in wind and solar power would see renewable sources of energy meeting the majority of increases in global energy demand. But a plateau for coal, along with rising demand for oil and gas, would mean global emissions continue to rise throughout the outlook period to 2040.

In contrast, the report’s “Sustainable Development Scenario” (SDS) sets out what would be required to give a 50% chance of limiting warming to 1.65C, which the IEA describes as “fully in line with the Paris Agreement”.

It says the SDS would require a “significant reallocation” of investment away from fossil fuels towards efficiency and renewables, as well as the retirement of around half the world’s fleet of coal-fired power stations and other changes across the global economy.

The IEA has this year also explored, but not modelled in detail, what it would take to limit warming to no more than 1.5C above pre-industrial temperatures, the aspirational goal of the Paris Agreement.

Future scenarios

The World Energy Outlook (WEO) is one of the most heavily scrutinised documents in the annual calendar of publications on the topic. Its hundreds of pages of analysis are based on thousands of datapoints, drawn from governments around the world, as well as the IEA’s .

The IEA says that it does not make forecasts in its outlook. Instead, it presents the consequences of societal energy “choices” in terms of CO2 emissions and other outcomes. The report explains:

“The World Energy Outlook does not aim to provide a view on where the energy world will be in 2030 or 2040. This will depend on hugely important choices that lie ahead. What the WEO-2019 does aim to do is to inform decision-makers as they design new policies or consider new investments or shape our energy future in other ways. It does so by exploring various possible futures, the ways that they come about, the consequences of different choices and some of the key uncertainties.”

The outlook spans three alternative “futures”, set out in the introduction and described in a , published ahead of the report’s release, on “understanding the WEO scenarios”.

The outlook’s central scenario is STEPS, which has “the intention to ‘hold up a mirror’ to the plans and ambitions announced by policymakers without trying to anticipate how these plans might change in future”. This includes the made by governments. The IEA does not assume that all policy goals will be met, however:

“[A]mbitions are not automatically incorporated into the scenario: full implementation cannot be taken for granted, so the prospects and timing for their realisation are based upon our assessment of the relevant regulatory, market, infrastructure and financial constraints.”

Stated policies include some net-zero emissions goals, such as . Similar goals agreed or under discussion, including in the EU, cover 12% of global emissions, the IEA says. This makes the targets significant, but not decisive, in terms of tackling the global emissions. But the IEA says there could be larger knock-on effects due to the technologies and approaches developed to meet net-zero targets, which could help others to also cut emissions.

The second WEO future is the “Sustainable Development Scenario” or SDS. This is a different type of scenario that starts from on energy access, air pollution and CO2 emissions before working backwards to show what would be needed to reach them.

Finally, the “Current Policies Scenario” (CPS) would see governments renege on their stated goals and intentions, with the energy system guided only by policies and laws that are already in place.

This year’s outlook continues to feature the CPS in its text, charts and data. But it is afforded lower priority, with the phrase “current policies scenario” used 102 times over 810 pages – far less often than the 793 mentions of the STEPS or the 535 for the SDS. The outlook says the CPS highlights the consequences of inaction and the level of effort required to meet even the STEPS pathway.

(For comparison, the CPS is mentioned 340 times in the , against 981 uses of the then-central NPS and 745 mentions of the “450 scenario”.)

Rising demand

On the basis of stated plans and policies around the world, the IEA says that global energy needs will continue to rise by 1% per year until 2040, adding demand equivalent to China’s current total.

This growth is driven by a rising population – based on the UN’s to reach 9 billion people by 2040 – and an expanding economy, with global GDP increasing by 3.4% a year, per projections.

The rate of energy demand growth is around half the average rate of 2% seen since 2000, the IEA says, due to shifts towards less energy-intensive industries, energy efficiency gains and “saturation effects” – for example, where demand for cars reaches a peak.

Some 49% of demand growth would be met by renewables in the STEPS, as shown with the red line in the chart, below. Gas use is also expected to rise rapidly (blue), overtaking coal to become the second-largest source of energy after oil and meeting a third of the rise in overall demand.

Global primary energy demand by fuel, millions of tonnes of oil equivalent, between 1990 and 2040. Future demand is based on the STEPS. Other renewables includes solar, wind, geothermal and marine. Source: IEA . Chart by Carbon Brief using .

In contrast to the rapid gains for gas and renewables, the IEA STEPS sees coal use plateau and then decline slightly from today’s levels (black line above). This confirms last year’s analysis that global coal demand peaked in 2014.

The IEA now also suggests that oil demand will start to level off by the 2030s (orange line) as a result of vehicle fuel-efficiency gains and the rise of electric vehicles (EVs), which see passenger car oil demand peak in the “late 2020s”. There are “profound questions” over the future of conventional cars, it says, given falling costs for EVs.

Oil demand for freight, shipping, aviation and chemicals “continues to grow”, the IEA says, with the growing popularity of SUVs another potential factor propping up demand. (Notably, documentation for the Saudi Aramco share sale also has global oil demand levelling off from around 2035.)

The global rise of SUVs is challenging efforts to reduce emissions.

If the appetite for heavier & bigger cars continues to grow at a similar pace to the past decade, this would add nearly 2m barrels a day in global oil demand by 2040.

4/

— Fatih Birol (@IEABirol)

Some two-thirds of the increase in global energy demand to 2040 comes from the Asia Pacific region, under the IEA STEPS. India becomes the country and its energy demand doubles, making it the single largest contributor to global growth and accounting for more than a quarter of the total increase.

Within this total, the STEPS sees rising offset large declines in the US and Europe. The IEA says:

“Coal is the incumbent in most developing Asian countries: new investment decisions in coal-using infrastructure have slowed sharply, but the large stock of existing coal-using power plants and factories…provides coal with considerable staying power in the STEPS.”

Shifting shares

The rise of renewables anticipated under the STEPS to 2040 is demonstrative of the “profound shifts” described by the IEA, yet it also points to the “slow moving” nature of the global energy system, as exemplified by the long, high plateau in demand for coal.

These shifting shares of demand growth are shown in the chart, below, with coal, oil and gas (shades of blue) having met most of the historical increases in energy use (leftmost columns).

While the STEPS maps a future where renewables meet half of the increase in demand to 2040, and the pace of growth slows due to shifting economic factors and energy efficiency (central columns), it remains well short of putting a cap on global CO2 emissions (see discussion below).

If increases in global temperatures are to be stopped, then even more decisive changes will be required, as shown in the example of the IEA SDS (rightmost columns).

Average annual change in global energy demand, by fuel, million tonnes of oil equivalent. Left: historical changes. Centre: IEA STEPS. Right: IEA SDS. Source: IEA . Chart by Carbon Brief using .

The rising portion of demand growth met by renewables sees the fossil fuel share of global energy use decline from 81% in 2018 to 74% in 2040 under the STEPS, or 58% under the SDS.

Moving from the STEPS to the SDS will require a wide range of changes, most of which have long been on the agenda for policymakers. As the report explains:

“The global value of fossil fuel consumption subsidies in 2018 was almost double the combined value of subsidies to renewable energy and electric vehicles and the revenue from carbon pricing schemes around the world. This imbalance greatly complicates the task of achieving an early peak in emissions.”

By the 2030s, investment in fossil fuels without carbon capture would halve in the SDS, relative to the average during 2014-2018. At the same time, investment in renewables, electricity networks and nuclear would roughly double and spending on energy efficiency would nearly quadruple.

This reflects the fact that energy efficiency is the single most important factor in tackling emissions, the IEA says, meaning that overall demand in 2040 under the SDS is slightly below today’s levels.

It says “the potential for efficiency improvements to help the world meet its sustainable energy goals is massive” and it has convened a to boost progress.

In part, this is a response to data showing that efficiency improvements are drying up and 2018 saw the , with this “faltering momentum” a cause for “deep concern”. It cites “a relative lack of new energy efficiency policies and of efforts to tighten existing measures”.

The World Energy Outlook is out today and shows once again the critical role of energy efficiency for achieving carbon goals.

— Jan Rosenow (@janrosenow)

Lower demand has knock-on consequences, particularly when combined with more rapid growth from renewables. Notably, demand for coal, oil and gas progressively declines under the SDS, with coal facing particularly large reductions (grey chunks in the rightmost columns, above).

Within this total, the IEA suggests that coal use in the power sector would be hardest hit. It says that more than half of current coal-fired power stations would retire by 2040 in the SDS, representing a fleet larger than .

With half of retirements coming before the end of their useful lives, some of the $1tn of capital invested in the world’s existing coal fleet would be put at risk, if warming is kept below 2C. Some 98% of the 222 gigawatts (GW) of coal in Europe and 88% of the 276GW in the US would close.

Under the SDS, the remaining coal plants would mostly need to be “repurposed or retrofitted”, the IEA says. This means they would either operate limited hours, during peaks in demand and troughs in renewable output, or would face substantial investments to fit (CCS) technology to prevent their CO2 emissions.

This year’s outlook contains new analysis on the methane released during coal mining, which it suggests has a greater warming impact than .

※ 全文及圖片詳見:()

作者

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務