Amzaon EC2虛擬化技術演進:從 Xen 到 Nitro

  今年2月,由光環新網運營的 AWS 中國(北京)區域和由西雲數據運營的 AWS 中國 (寧夏)區域發布新的實例類型,新的實例類型包括 C5、C5d、R5、R5d。除了這四種之外,在AWS國外部分區域還上線了最新的C5n。       這些新實例類型個個都具有鮮明的特徵,我簡單整理歸納如下:

  • C5實例:性價比顯著提升(與 C4 實例相比,C5 實例提供了更高的內存與 vCPU 比率,並且性價比提高了 25%,某些應用程序提高了 50% 以上),更大的實例大小(C5 實例新的更大的實例 c5.18xlarge提供了 72 個 vCPU 和 144 GiB 內存並提供了 25 Gbps 的網絡帶寬)。
  • C5d實例:基於本地 NVMe 的 SSD 磁盤將被物理連接到主機服務器,提供與C5實例的生命周期相耦合的塊級存儲。c5d.18xlarge 規格的實例支持2塊900GB的NVMe SSD作為本地存儲。
  • C5n實例:這是C5 系列的最新成員,其c5n.18xlarge規格可提供高達 100Gbps 的網絡吞吐量。
  • R5實例:其最大實例規格支持96 vCPU、768 GiB內存和25 Gbps 網絡帶寬。
  • R5d實例:R5d 實例與 R5 實例規格相同,它還包括高達 3.6 TB 的本地 NVMe 存儲。

這些實例類型之所以如此實力超群,我認為主要歸功於兩點:

  • 處理器升級

C5 實例配備 Intel Xeon Platinum 8000 系列 (Skylake-SP) 處理器,它發佈於2017/Q3,具有高達 3.4GHz 的穩定全核 Turbo CPU 時鐘速度,並使用 Intel Turbo Boost Technology 來允許單個核心睿頻高達3.5GHz。C5 實例為新的 Intel 高級矢量擴展 512 (AVX-512) 指令集提供了支持,與上一代 C4 實例相比,矢量和浮點計算性能提高最高可達2倍。
而發佈於2015年的C4 實例類型,配備Intel Xeon E5-2666 v3 (Haswell) 處理器。其時鐘頻率為2.9 GHz,配合Intel® Turbo Boost后最高可達3.5 GHz。

  • 採用了AWS Nitro 虛擬化平台

AWS Nitro 將是這篇文章的主角。本文會從它的發展歷程、架構、所創造的價值等方面進行分析和介紹,試圖總結出AWS上虛擬化基礎平台發展的脈絡。

AWS EC2虛擬化發展歷程

下錶總結了AWS曾經採用的虛擬化技術,以及這些技術之間的性能對比:

 

  • #1是全模擬技術。這種虛擬化方式能支持未修改的客戶機操作系統,但速度會嚴重下降。典型產品是VMware 在1986年發布的虛擬化產品。AWS 並沒有採用這種虛擬化技術,放在表格中只是為了做對比用。
  • #2 是基於Xen的半虛擬化技術(Paravirtualization,PV)。PV 要求修改客戶機內核和驅動。EC2第一個採用半虛擬化的實例類型是 m1.small。
  • #3 到 #6 是基於Xen和CPU硬件的全虛擬化技術(Hardware-assisted virtualization,HVM)。採用Xen HVM 技術的虛擬機運行在具有CPU和內存(VT-x)硬件虛擬化能力的處理器上,並使用半虛擬化驅動程序用於網絡和存儲設備。HVM 3.0 中尚未實現中斷和定時器半虛擬化,但在4.0中已有改善。
  • #7 和 #8 則是AWS Nitro技術,這是AWS 研發的一種新虛擬化平台。後面會有詳細介紹。

過去幾年中,Xen是AWS上虛擬化技術的主體,業已成為業界標準之一,已經非常成熟。那麼,為什麼AWS要從Xen 向 Nitro 發展呢?這得從Xen 的架構說起。

從上圖可以看出,Xen 實現了虛擬機的CPU 和內存虛擬化,但是虛擬機的I/O 訪問,包括網絡和存儲等,都是通過虛擬機中的前端模塊和 dom0 中的後端模塊通信,然後由dom0 中的後端模塊通過設備驅動實現的。這I/O路徑太長,這降低了I/O性能,而且dom0還會和業務虛擬機搶佔宿主機資源,很難實現管理虛機和業務虛機之間的平衡,以及避免抖動。

2013年,AWS 採用 Xen PV虛擬化技術的cr1.8xlarge 實例的架構如下圖所示:

這是嚴格意義上未採用Nitro技術的最後一個EC2型號。簡要說明:

  1. 圖中的硬件(Hardware),是運行虛擬機的物理服務器,採用了當時很強大的標準的10Gbps網卡,以及管理一些本地磁盤的存儲HBA卡。Hardware上既運行用戶的業務虛擬機,還運行Xen的dom0虛擬機。
  2. VMM採用Xen項目的PV模式。
  3. 圖中 Amzon Linux 代表Xen dom0,它負責訪問硬件,向虛擬機提供I/O 能力。

圖中 cr1.8xlarge 代表一個這種規格的虛擬機,它的本地存儲、EBS卷和VPC網絡訪問都是通過Xen管理的dom0 虛擬機實現的。

Nitro起源和發展

針對傳統虛擬化架構存在的問題,從2012年開始,AWS EC2虛擬化團隊就開始思考以下問題:

  1. 能做出比純軟件架構更好的hypervisor嗎?
  2. 設備模型本身很複雜,而且它會和業務虛擬機競爭CPU和系統資源,同時技術上它很難避免抖動發生
  3. hypervisor太重了,能將hypervisor 和它周邊的組件解耦嗎?

  從成立之日起,AWS就善於聽取客戶的呼聲和建議,並不斷進行迭代式改進,而不是大刀闊斧地從頭設計一個新架構。根據該原則,AWS團隊首先從最難的網絡部分着手,其位置就是上圖中的金黃色虛線框所示位置。從2013年開始,一些EC2實例類型開始支持網絡接口的硬件虛擬化:單根I/O虛擬化(SR-IOV),而第一個是2013年1月發布的C3,它首次採用了AWS增強型網絡(enhanced networking)。這最初是通過ixgbe驅動程序實現的,速度高達10 Gbps。   c3.8xlarge的架構如下圖所示:

c3.8xlarge的架構與cr1.8xlarge相比,在宿主機上增加了一塊新網卡,這塊網卡和原有的標準網絡通過一個迴環線(loopback cable)連接起來。虛機VPC網絡功能不再通過Xen 的dom0 實現,而是直接訪問宿主機上的這塊硬件網卡。C3 是AWS EC2 歷史上增長最快的幾個實例類型之一,它尤其以控制性能抖動和持續的網絡性能著稱。這可以看做Nitro思想的發源,那就是將軟件功能卸載到專有硬件上。

  下一個改進方向是EBS存儲訪問性能提升。   2015年,AWS推出了C4實例類型,它針對EBS卷使用了硬件虛擬化技術。c4.8xlarge的架構如下圖所示。仔細對比能發現,這個新架構與C3中的網絡架構改進有些不同。在虛擬機中,還保留了“前端-後端”這種Xen傳統架構,這是當時為了兼容性和穩妥新考慮,因為NVMe在當時來說還是一種非常新的技術。在宿主機上,採用了新收購的Annapurna Labs公司開發一種卡(下圖中黃色虛線框內),它能將遠端存儲以NMVe形式呈現給虛擬機。

這個改進的結果是,宿主機上的CPU被Xen佔用得少了,能更多地被虛機使用了。

  2016年5月發布的X1 是第一個支持ENA的實例類型。ENA是增強型網絡的最新實現,速度高達25 Gbps。ENA,全稱是Elastic Network Adapter,它正是Nitro項目的一部分,它是由Annapurna Labs公司開發的。

現在的ENA,能用於虛擬機和物理機,它以開源項目形式發布在github上。ENA 是AWS網絡虛擬化一關鍵技術,它使得虛擬機能夠繞過內核和用戶空間網絡處理程序,直接操作網卡硬件,這顯著提升了網絡效率。

從用戶使用角度,也許只是用了一個新網卡驅動。但是其底層採用了Annapurna Labs公司開發的定製網絡ASIC硬件卡。這是Nitro第一款真正的專用硬件卡。它不僅卸載了VPC網絡功能,還卸載了EBS 存儲網絡功能。因此這是一種完全的網絡負載卸載硬件。

 

下一步的優化方向在實例存儲上。2017年,AWS發布了存儲優化實例類型i3,它使用了SR-IOV和NVMe存儲驅動。這是AWS首次採用Annapurna Labs研發的Nitro存儲卡40202所管理的SSD磁盤,這些磁盤被直接映射給虛擬機,虛擬機通過NVME驅動來使用宿主機上的SSD磁盤。這能實現磁盤300萬以上的IOPS性能。Nitro 芯片負責包括磁盤監控、加密、QoS等職責。  

 

  顯然,到這時候為止,仍然剩下的問題只能是Xen 自身,以及它的管理功能部分了。Xen過於笨重,因為作為傳統 Hypervisor,它必須做很多事情 – 它必須保護物理硬件和 BIOS,它必須虛擬化 CPU,虛擬化存儲,虛擬化網絡,並提供豐富的管理功能。其管理性dom0虛擬機會搶佔業務虛機的系統資源。那到底能不能把Xen徹底替換掉呢?答案是肯定的,因為AWS在技術上從來沒讓人失望過。   2017年11月,AWS發布了C5 實例類型。它使用基於KVM的Nitro hypervisor 替換了Xen,hypervisor 軟件大大被簡化,Xen 所用的 dom0 也不需要了。其架構示意圖如下:

 

AWS Nitro 則重新構建了EC2虛擬化基礎架構。Nitro 系統將存儲、網絡和安全功能卸載(offload)到專用的硬件(Nitro卡)上,帶來的好處是虛擬化實例幾乎可以為客戶機操作系統提供主機的所有 CPU 和內存,同時Hypervisor 的功能也因此大大減弱。   Nitro 還被用到2017年發布的AWS 首個物理機實例類型 i3.metal中。下圖是i3.metal架構示意圖:

在i3.metal 中,Nitro 發揮了基礎性作用。它的安全芯片通過提供硬件保護和固件驗證功能為I3實例提供安全保障;它的各種卡,使得I3實例具備基於非易失性存儲器標準 (NVMe) SSD 的實例存儲,通過ENA支持高達 25Gbps 的聚合網絡帶寬。 

Nitro 架構

AWS Nitro 系統是模塊化組件的集合,可以使用廣泛的計算、存儲、內存和網絡選項來設計 EC2 實例,為新一代EC2實例提供動力。它包括三大部分:

 

Nitro 卡

 

這些Nitro 卡是硬件,插入到宿主機的PCIe卡槽中,採用SR-IOV 直通(passthrough)技術將這些卡呈現給實例。包括:

  • VPC Data Plane(用於VPC訪問的Nitro卡):本質上是一塊通過PCIe附加到宿主機上的一塊定製網卡,支持網絡封包和解包、安全組、限速器和路由等功能。實例通過ENA驅動和它通信。同時,該卡還帶有一些網絡加速功能。以限速器為例,每個Nitro支持的實例,不管它在哪個區域哪個數據中心哪個宿主機上,都會有一致的性能,這對分佈式應用非常重要。
  • EBS Data Plane(用於EBS卷訪問的Nitro卡):本質上是一塊通過PCIe附加到宿主機上的一塊定製卡。通過該卡,遠端存儲被以NVMe設備形式展現給實例,實例通過標準NVMe驅動程序訪問該卡。它首次被用在C4中。支持卷加密、存儲加速;支持I3裸機實例。
  • Instance Storage Data Plane(用於實例存儲訪問的Nitro卡):通過該卡,本地磁盤被以NVMe設備形式展現給實例,實例通過標準NVMe驅動程序訪問這些磁盤。支持加密、限速器和本地磁盤監控。

除了卡之外,Nitro 還提供卡控制器(Card Controller)。它提供API端點,負責協調所有Nitro卡、Nitro Hypervisor和Nitro安全芯片。它還利用Nitro安全芯片實現了Hardware Root Of Trust(硬件信任根),支持實例監控、計量和認證。它還為Nitro EBS卡實現了NVMe控制器。

Nitro 安全芯片

Nitro安全芯片整合到宿主機主板中,控制對所有非易失性存儲的訪問,持續監控和保護硬件資源,並在每次系統啟動時獨立驗證固件。

Nitro hypervisor

Nitro hypervisor位於極簡化的定製的Linux 內核中,基於KVM,帶有定製的VMM和小用戶空間應用。它只負責管理內存和CPU分配,將Nitro卡虛擬功能分配給實例,監控和計量硬件等,不再需要提供任何網絡功能。因此它只需執行虛擬機所需指令,快速而且簡單,在大多數工作負載中能提供接近裸機的性能。 Nitro 各組件之間的關係如下圖所示:

 

Nitro 帶來的豐富價值

更高網絡訪問性能

利用Nitro提供的新一代 Elastic Network Adapter (ENA) 和 NVM Express (NVMe) 技術,C5 實例提供了高達 25 Gbps 的網絡帶寬和更低延遲及抖動。2018年發布的更強大變體 C5n 實例,支持網絡帶寬高達 100 Gbps,用戶的仿真、內存緩存、數據湖以及其他通訊密集型應用運行得將比以往更好。   採用Nitro增強網絡功能后的網絡延遲對比:

(Series 1:cc2.8xlarge,2:c3.8xlarge,3:c4.8xlarge,4:c5.18xlarge,5:c5.18xlarge(採用ENAv2))

網絡和存儲帶寬對比:   (1:c3.8xlarge,2:c4.8xlarge,3:c5.18xlarge,4:c5n.18xlarge. Series1:網絡,Series2:存儲)

更高EBS和本地存儲訪問性能

Nitro 使得實例可通過物理方式連接到主機服務器的基於 NVMe 的本地 SSD 塊級存儲,以及將遠端存儲以NVMe設備的形式呈現給實例。 2019年3月,由Nitro支撐的新計算密集型 C5 和 C5d 實例已經在AWS 北京和寧夏區域推出。C5實例支持高達9Gbps 的專用 Amazon EBS 帶寬。而 C5d 最大實例規格則可使用兩塊900G的NVMe SSD。這些實例非常適合需要訪問高速、低延遲的本地存儲的應用程序。

更大實例大小和CPU內存比率

由Nitro支撐的C5實例,其實例的CPU和內存比率,由C4的1:1.875上升到1:2;實例的最大規格,從C4的36vCPU/60Gib內存,上升到72vCPU/144Gib內存。

更低虛擬化花銷

Nitro Hypervisor 是一款輕薄的靜態的虛擬機管理程序,可管理虛擬機的內存和CPU分配,並提供與大多數工作負載無法區分的性能。據Netflix公司Brendan Gregg 觀察,Nitro Hypervisor的性能損耗非常小,通常不到1%,他的結論是 Nitro提供的虛擬化性能接近裸設備。

更低Hypervisor抖動

有了Nitro后,就不再需要為存儲和網絡I/O再預留CPU和內存資源了。這不僅使得可以向EC2實例分配更多資源,為更大的實例規格提供了可能,還為實現一個簡單的輕量的hypervisor提供了可能,而這就為實現更低hypervisor抖動創造了條件。   下圖是一AWS 客戶在三種EC2實例上採用對延遲要求極低的一實時應用做的對比測試。藍色是C5,紅色是i3.metal,黃色是C4。SLA 是用於測試的實時應用所能忍受的最高延遲。

 

從上圖中的測試結果看,C5 相對裸機只有一點極小的附加開銷,而且性能非常平穩,幾乎沒有波動,能完全滿足應用的SLA需求。而C4則有相對較大的波動,只能大概滿足70的SLA。

更多實例類型

AWS發布了基於Nitro的實例存儲實例類型 C5d,M5d 和 R5d,提供低延遲高吞吐的基於NVMe的實例存儲。 AWS在2017 re:Invent上宣布了基於Nitro的AWS EC2 Bare Metal實例 I3.metal。它沒有性能開銷,能夠運行你喜歡的任何東西,比如Xen,KVM,容器,ESXi,FireCracker微虛機等;支持非虛擬化環境,支持容器環境,同時還能繼續使用比如EBS、ELB和VPC等基礎服務;支持比如SAP HANA和其它內存型應用。 AWS還基於Nitro發布了採用AMD EPYC處理器的系列實例R5,M5和T3,最高可降低10%成本。 AWS發布了基於Nitro的具有100Gbps網絡帶寬的實例類型C5n,這是運行HPC和分佈式機器學習負載的理想類型。 AWS發布了基於Nitro的採用AWS Graviton(基於ARM)處理器的實例類型A1,最高可降低45%成本。

更低價格、更高性價比

下錶显示了AWS 北京(BJS)和中衛(ZHY)區域的4代和5代EC2實例的價格比較,你可以看到實實在在的價格下降:  

 

目前,Nitro支撐的C5 實例提供了 EC2 產品系列中最佳的價格/計算性能比。與C4實例相比,其性價比提高了49% 。 與R4實例相比,由Nitro支撐的R5實例為每個vCPU提供額外5%的內存,且每 GiB 價格低50%。R5實例非常適用於高性能數據庫、分佈式內存緩存、內存數據庫和大數據分析等應用程序。

為更多性能優化提供了可能

對於需要深度定製化EC2 的用戶而言,Nitro 還帶了了另外的好處:對於EC2 更深入的監控和優化。在由Nitro支撐的C5實例中,你可以得到數百個PMC 計數器(Performance Monitoring Counters ,性能監控計數器)。作為對比,以前的實例類型中,你只能看到區區7個PMCs。更多的PMC計數器,為性能優化提供了更多可能。

小結

亞馬遜 AWS CTO 沃納·威格爾(Werner Vogels)曾經說過,“在亞馬遜 AWS,我們90%到95%的新項目都是基於客戶給我們的反饋,剩下5%也是從客戶角度出發所做得創新嘗試。”而Nitro 正是這種項目之一,它誕生於2013年,成年於2017年,現在還在不斷成長中。Nitro 正在作為AWS核心虛擬化架構平台,推動着AWS最核心的EC2產品家族不斷往更大(單實例的vCPU和內存更大)、更快(I/O速度更快)、更安全(採用Nitro安全芯片)、更穩定(Hypervisor抖動更低)、更多類型、更高性價比方向演進,支撐越來越多用戶越來越多的業務場景,創造着越來越大的業務價值。     主要參考文檔:

  1. AWS re:Invent 2018: Powering Next-Gen EC2 Instances: Deep Dive into the Nitro System (CMP303-R1)
  2. AWS re:Invent 2017: C5 Instances and the Evolution of Amazon EC2 Virtualization (CMP332)
  3. AWS re:Invent 2018: Deep Dive on Amazon EC2 Instances & Performance Optimization Best Practices (CMP307)
  4. AWS re:Invent 2018:Optimizing Network Performance for Amazon EC2 Instances (CMP308-R1) 

感謝您的閱讀,歡迎關注我的微信公眾號:

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

反射機制 小小談

反射機制(Reflection)

何為反射

反射是在兩種物質分界面上改變傳播方向返回原來物質中的現象
反射是生物體對外界刺激做出應激行為的過程,根據產生的原因分為條件反射非條件反射等,典型的實驗案例包括巴甫洛夫的狗……
反射是一些面向對象程序設計語言提供的針對對象元數據(Metadata)的一種訪問機制

元……數據??什麼高深莫測的武功??

啊,誠然,一旦涉及到“元XXX”事情通常就開始變得無比抽象,以至於我不禁念叨起那句訣

太極生兩儀,兩儀生四象,四象生八卦……

不過元數據這個概念在數據庫里還是比較常見的,比如,某個關係型數據庫里有張表:

水果

編號 名字 數量
1 蘋果 6
2 香蕉 3
3 5
4 橘子 3
5 菠蘿 2

數據,就是存在表裡的一條一條的記錄,(1,蘋果,6),(3,梨,5)都是數據,那麼,元數據就是凌駕於這些數據之上的用於描述數據數據,對於這張表而言,也就是這張表的表頭(關係數據理論里稱之為關係模式):(編號,名稱,數據)

划重點
元數據(Metadata):用於描述數據的數據

好像有些明朗了,但那關面向對象什麼事呢

眾所周知,類(Class)是面向對象的一個重要概念,儘管,針對於數據庫來說,對象模型和關係模型是不同的概念(上文提到的是關係模型的一個例子),但是,對象模型中的對象和關係模型中的關係,其級別是等同的。

關係……又對象……越來越聽不懂了

好吧,我們先把關係放在一邊,我們只把上邊的東西看做一張表。

難道你就沒有把它改寫成如下形式的衝動嗎??

public class Fruit
{
    public int no;
    public string name;
    public int count;
    
    public Fruit(int no, string name, int count)
    {
        // ...
    }
}

好了,上面的類定義的語義就是

有這樣一類東西,我們稱呼這類東西為水果,結構如下……

那麼,這樣一來,我們就可以定義一個no為9,name叫做“西瓜”,count為5的一個對象,這個對象具有具體的數據。

而上面的類定義代碼,包含的就是這個類的元數據

說的再直白點吧

以人為例,數據注重的是這人的臉長啥樣,而元數據注重的是這人有沒有臉(好像不太對……)

好吧差不多了解了,但元數據和反射有什麼關係呢

反射是一些面向對象程序設計語言提供的針對對象元數據(Metadata)的一種訪問機制

本文一開始就說了,罰站20年

不過在此之前先解釋一件事,元數據在哪

任何一個面向對象的程序設計語言,其類類型都具備一個元數據的存儲,至少程序會使用這個元數據能夠動態地構造此類的對象。但不同的語言機制不同,比如C++這種的,因為直接和系統進行愉♂快的互♂動,因此元數據就直接使用系統的內存地址了,這種數據使用是很不直觀的,同時也不使用任何託管機製做後援(巨硬魔改的C++/CLI不在討論範圍內),因此這種貼近底層的語言不支持反射機制,雖然可以通過強行向程序代碼中通過工廠類模式強行注入可讀的元信息(方法參見這位大佬的文章)。

但是,正如前面所說的,如果元數據在託管編譯或解釋的狀態下會保留一份可讀的版本,這是提供給解釋器或者託管平台用的,當然,這種情況下語言一般會提供一個較為完善的元數據訪問機制,這就是反射。這類語言典型的代表就是C#(.NET託管)、Java(JVM虛擬機)、Python(解釋器提供)等。

那……反射是如何運作的呢??

反射嘛。那還不容易,拿個鏡子就可以了呀!
或者用羊角錘偷襲的方式砸膝蓋什麼的也是很容易的呀!
不過這麼說來,拿羊角錘偷襲鏡子豈不是更棒!!

正如之前所說,反射機制是對類的元數據的獲取和操縱,因此,一個重要的前提就是:

這個程序設計語言的運作機制當中,類的元數據必須是可見的,如果可讀的話那更好

只有當類的元數據是可見的,反射機制才有訪問它們的可能,但是元數據的可讀性會決定反射機制訪問它們的難易程度。

這裏補充一句,有人會說,在使用IDE或者代碼編輯器的時候,我們寫object.property這種訪問方式的時候編譯器不就直接告訴我們了么??
關於這一點,這裏暫時只說一個前提:

反射機制的實際動作是聚焦於運行時(Runtime)的。

在程序代碼編譯之前我們恣意地書寫這MyObject.id.hashCode.getFlush().balabala的時候,這是預編譯的過程,預編譯的時候當然這些元數據都是以字面形式給出的(因為你的代碼里寫了這個類的定義),你可以非常愉悅地Ctrl+C Ctrl+V或者享受着IntelliSense帶給你的N倍快樂,這個時候再談反射就沒什麼意義了,因此,反射機制訪問元數據都是在編譯后運行時發生的。

明明都是面向對象,為什麼偏偏C++不支持這個東西呢

以C++為例,這些元數據是否可見?答案是肯定的,那為什麼不支持反射機制呢,因為這些元數據是以指針的方式給出的,指針在已編譯的C++程序中的存在形式就是地址,說的再粗暴點,就是4或8字節的二進制數……
也就是說,在已經編譯完成的C++程序的眼裡,類的元數據已經變成二進制的地址碼了,如果某人在沒有源代碼的情況下想給這個項目寫一個反射機制,那麼他將不得不面對一大堆的:

0xb08dfe231a1c002e
0xb08dfe231bc128f6
0xb08dfe2417a90f5d
......

看到這些,他長舒了一口氣,優雅地點燃了一根香煙,然後毫不猶豫地戳到電腦屏幕上:

鬼知道這是什麼玩意啊!!

如果原項目加個殼、模板元編一下再做個混淆加密的話那更沒法看了,因此如果一定要實現反射機制,一般都是把反射機制直接囊括到項目開發過程當中(就像上面那位大佬的文章中提到的那樣,原項目的作者也是反射機制的構造者)。
這樣的話就會存在一個上上上個世紀汽車行業出現的問題:

這輛車的件無法用到另外一輛車上!
這個反射機制無法用到別的項目上!

當然,這樣說可能有些絕對,但以C++的方式實現一個能夠廣泛用於所有項目的反射機制應該是極端困難的。
上面大佬的文章當中,這個C++的項目要使用反射機制,是藉助工廠模式實現的,關於這些的實現方法,詳見大佬文章(當然我自己也沒完全看懂)

那託管語言又如何呢

C#、Java,這兩種語言都是託管代碼的(C#使用.NET進行託管,Java則交給了JVM虛擬機)。

與C++不同的是,他們並不直接接觸系統底層,而是通過中間代碼訪問底層的。

中間代碼由誰處理呢,C#是通過.NET提供的CLR,產生的中間語言是程序集,而Java靠的是JVM,其中間產物是class文件。
如果有幸使用一些IDE打開這兩個文件往裡窺探一遭的話,我們應當不難從中找到這些元數據的信息。

這就好像,一群孩子進了幼兒園,一個託管老師全程進行看護。

把拔碼麻區上辦,我區悠貳園吶

當然,託管老師肯定是知道孩子叫什麼名的,訪問他們自然也是很容易的。同理,託管環境(或虛擬環境)也是一樣的,因為銜接上下兩層,因此把底層的元數據和上層的可讀文本構造反射的橋樑是很容易辦到的,因此,C#和Java都提供了一套非常完善的反射庫,他們可以被用於使用這兩種語言寫的任意一個類當中。

好了,道理我都懂,但為什麼要反射呢?

反射能幹什麼呢

舉個最簡單的例子

我……我有一個夢想,我想要這樣一個函數,能夠返回Person類是否有我所說的方法,但是我不知道Person類里有什麼,比如我想問他有沒有Eat()方法,它返回true,我問他有沒有Fly()方法,它能返回false

好了,換作是你,你會怎麼實現這樣一個函數呢??

而反射機制恰恰做到了!
你提供給反射機制一個字符串形式的函數名,反射機制不僅可以得知這個函數是否存在,甚至能幫助你去執行這個函數(Invoke)。

什麼,你不好問它有沒有某個函數??好啊,反射機制甚至可以告訴你這個類都有哪些屬性哪些函數,繼承自誰,可見性如何,是否抽象等等。

那反射在什麼時候比較好用呢

上面那個例子其實就是一個經典的用途。

或者,我們可以考慮另外一個場景。

你寫了某個函數接受了一個抽象為Object的對象,你希望,如果Object的對象存在方法Grow則調用之,否則什麼也不做。

這個時候首先可以通過反射機制確定方法是否存在,但即便方法已經存在,我們是無法直接調用的,因為對象已經抽象為Object,而Object並不存在方法Grow,所以直接調用就洗洗睡了。

我們不能具象回來么??

如果我們知道類在抽象之前是什麼類型的時候,那當然可以具象化回來。
但是抽象雖然發生於編譯時或運行時(動態創建的對象),但具象類型的獲知卻是在編譯之前的代碼源文件,而且還有些時候你根本無法知道原類型,那也沒辦法拆箱。


這裏面我為了方便,也是想不出啥更好的詞
這裏我稱派生類基類的多態轉化為抽象
反過來的過程稱為具象

那我還怎麼調用Grow

反射機制可以獲取到完整的可用方法的列表,我們在列表中找到了Grow,存在形式為Method/MethodInfo對象或乾脆就是個字符串。

但無論是哪種,obj.Grow();是不可能了,好在反射機制連這件事都考慮在內了——Invoke調用!!

反射機制不僅知道你想要什麼方法,還可以幫助你調用這個方法,這個調用就通過一個叫做Invoke的方法完成。

不同語言對Invoke的定義不盡相同但功能上大同小異,通過Invoke調用某方法的過程實質上是轉調回調(或者是間接調用)。
間接調用比直接調用更加的強大靈活,但繞了遠路。

還有什麼比較宏大一點的應用么??

宏大一點……好吧,其實每一個磅礴的工程都是從一點一滴做起的。

一個很經典的案例,就是上文那篇大佬文章里的一個常用功能——序列化(Serialization)
雖然C#和Java本身就有可以用於序列化的一些結構和功能庫(Serializable接口之類的),但是有些時候我們對序列化機制如果有更高的可定製性要求的時候,我們往往傾向於自己構建一套心儀的序列化功能庫。

於是乎就有一個最簡單的問題擺在面前:

現在有Class1類的對象,還有Class2類的對象,還有Class3類的一些對象想要轉化成可解析的內容,以供發送或保存(當然這也就意味着,這些對象的所有屬性和狀態都要保存),但是這老大老二老三一家一個樣,屬性也各不相同,我又不想挨個單獨寫,那該怎麼辦呢??

現在有了反射機制,問題就很容易解決了。三胞胎嫌分起來麻煩??反射機制可以把他們安排的明明白白!!你可以向反射類提供一個完整的類名,反射機制就能保證給你這個類對應的可用屬性的列表,以及一整套處理方案(Get和Set),之後還不是想來啥來啥,美滋滋~~

當然,以上都是反射機制用途中小的不能再小的冰山一角,比如我還可以通過反射機制根據我的輸入創建我想要的類型的對象等等。

哇,反射這麼強大??我要滿地反射!!

冷靜點!任何事物都有多面性,反射也不例外,我們看看反射機制有什麼特點,它到底是否適合所有情形。

極致靈動(Flexi Frenzy) 稀有屬性

反射機制可以讓你的代碼非常靈活,以不變應萬變。
這也正是反射機制帶來的最大的好處。

未卜先知(Fortune Tell) 普通屬性

反射機制是在運行時起作用,當然,運行期間發生什麼,編譯之前是無法獲知的,反射就是處理這件事的。

效率捉急(Emaciated Efficiency) 糟糕屬性

反射機制最大的問題!

反射機制的效率是十分低下的,首先在運行時獲取元數據再轉化成可讀形式就不是一個很快的過程,而反射的Invoke調用是個不折不扣的間接調用。

不當地使用大量反射會導致程序效率的急劇下降。

代碼膨脹(Code Expansion)

顯然,用反射進行調用的代碼往往比直接調用寫起來複雜,所以除非你寫代碼是按行數計工資,否則能直接調用就不要反射。

健壯風險(Robustness Risk)

反射機制一般允許用戶傳入字符串……

然後就是萬劫不復深淵之伊始

這時候用戶傳的字符串就可以非常的五花八門了,就好像一個動物園裡,反射機制是一個可愛的小動物,而遊客開始不分青紅皂白地對它投各種食,良莠不齊,可是你的反射機制很脆弱,它可禁不起這折騰,吃到不好的東西就會生病罷工(拋異常,然後中止),因此你這當奶媽奶爸就要多操心,幫它收拾(捕獲),告訴他如何分辨食物(預先判斷)……

不過呢,有些時候引入反射機制恰恰就是出於健壯性的考慮……

如果我養的不是個反射機制而是一隻熊貓的話我會上天的!!

總結

反射是個強大的武器,但使用應多加謹慎!

以上

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

詳解二分查找算法

我周圍的人幾乎都認為二分查找很簡單,但事實真的如此嗎?二分查找真的很簡單嗎?並不簡單。看看 Knuth 大佬(發明 KMP 算法的那位)怎麼說的:

Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…

這句話可以這樣理解:思路很簡單,細節是魔鬼。

本文就來探究幾個最常用的二分查找場景:尋找一個數、尋找左側邊界、尋找右側邊界。

而且,我們就是要深入細節,比如while循環中的不等號是否應該帶等號,mid 是否應該加一等等。分析這些細節的差異以及出現這些差異的原因,保證你能靈活準確地寫出正確的二分查找算法。

一、二分查找的框架

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = (right + left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

分析二分查找的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。本文都會使用 else if,旨在講清楚,讀者理解后可自行簡化。

其中…標記的部分,就是可能出現細節問題的地方,當你見到一個二分查找的代碼時,首先注意這幾個地方。後文用實例分析這些地方能有什麼樣的變化。

另外聲明一下,計算 mid 時需要技巧防止溢出,建議寫成: mid = left + (right – left) / 2,本文暫時忽略這個問題。

二、尋找一個數(基本的二分搜索)

這個場景是最簡單的,可能也是大家最熟悉的,即搜索一個數,如果存在,返回其索引,否則返回 -1。

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意

    while(left <= right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}

1. 為什麼 while 循環的條件中是 <=,而不是 < ?

答:因為初始化 right 的賦值是 nums.length – 1,即最後一個元素的索引,而不是 nums.length。

這二者可能出現在不同功能的二分查找中,區別是:前者相當於兩端都閉區間 [left, right],後者相當於左閉右開區間 [left, right),因為索引大小為 nums.length 是越界的。

我們這個算法中使用的是 [left, right] 兩端都閉的區間。這個區間就是每次進行搜索的區間,我們不妨稱為「搜索區間」(search space)

什麼時候應該停止搜索呢?當然,找到了目標值的時候可以終止:

    if(nums[mid] == target)
        return mid; 

但如果沒找到,就需要 while 循環終止,然後返回 -1。那 while 循環什麼時候應該終止?搜索區間為空的時候應該終止,意味着你沒得找了,就等於沒找到嘛。

while(left <= right)的終止條件是 left == right + 1,寫成區間的形式就是 [right + 1, right],或者帶個具體的数字進去 [3, 2],可見這時候搜索區間為空,因為沒有数字既大於等於 3 又小於等於 2 的吧。所以這時候 while 循環終止是正確的,直接返回 -1 即可。

while(left < right)的終止條件是 left == right,寫成區間的形式就是 [right, right],或者帶個具體的数字進去 [2, 2],這時候搜索區間非空,還有一個數 2,但此時 while 循環終止了。也就是說這區間 [2, 2] 被漏掉了,索引 2 沒有被搜索,如果這時候直接返回 -1 就可能出現錯誤。

當然,如果你非要用 while(left < right) 也可以,我們已經知道了出錯的原因,就打個補丁好了:

//...
while(left < right) {
    // ...
}
return nums[left] == target ? left : -1;

2. 為什麼 left = mid + 1,right = mid – 1?我看有的代碼是 right = mid 或者 left = mid,沒有這些加加減減,到底怎麼回事,怎麼判斷?

答:這也是二分查找的一個難點,不過只要你能理解前面的內容,就能夠很容易判斷。

剛才明確了「搜索區間」這個概念,而且本算法的搜索區間是兩端都閉的,即 [left, right]。那麼當我們發現索引 mid 不是要找的 target 時,如何確定下一步的搜索區間呢?

當然是去搜索 [left, mid – 1] 或者 [mid + 1, right] 對不對?因為 mid 已經搜索過,應該從搜索區間中去除。

3. 此算法有什麼缺陷?

答:至此,你應該已經掌握了該算法的所有細節,以及這樣處理的原因。但是,這個算法存在局限性。

比如說給你有序數組 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,沒錯。但是如果我想得到 target 的左側邊界,即索引 1,或者我想得到 target 的右側邊界,即索引 3,這樣的話此算法是無法處理的。

這樣的需求很常見。你也許會說,找到一個 target 索引,然後向左或向右線性搜索不行嗎?可以,但是不好,因為這樣難以保證二分查找對數級的時間複雜度了。

我們後續的算法就來討論這兩種二分查找的算法。

三、尋找左側邊界的二分搜索

直接看代碼,其中的標記是需要注意的細節:

int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意

    while (left < right) { // 注意
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return left;
}

1. 為什麼 while(left < right) 而不是 <= ?

答:用相同的方法分析,因為初始化 right = nums.length 而不是 nums.length – 1 。因此每次循環的「搜索區間」是 [left, right) 左閉右開。

while(left < right) 終止的條件是 left == right,此時搜索區間 [left, left) 恰巧為空,所以可以正確終止。

2. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:因為要一步一步來,先理解一下這個「左側邊界」有什麼特殊含義:

對於這個數組,算法會返回 1。這個 1 的含義可以這樣解讀:nums 中小於 2 的元素有 1 個。

比如對於有序數組 nums = [2,3,5,7], target = 1,算法會返回 0,含義是:nums 中小於 1 的元素有 0 個。如果 target = 8,算法會返回 4,含義是:nums 中小於 8 的元素有 4 個。

綜上可以看出,函數的返回值(即 left 變量的值)取值區間是閉區間 [0, nums.length],所以我們簡單添加兩行代碼就能在正確的時候 return -1:

while (left < right) {
    //...
}
// target 比所有數都大
if (left == nums.length) return -1;
// 類似之前算法的處理方式
return nums[left] == target ? left : -1;

3. 為什麼 left = mid + 1,right = mid ?和之前的算法不一樣?

答:這個很好解釋,因為我們的「搜索區間」是 [left, right) 左閉右開,所以當 nums[mid] 被檢測之後,下一步的搜索區間應該去掉 mid 分割成兩個區間,即 [left, mid) 或 [mid + 1, right)。

4. 為什麼該算法能夠搜索左側邊界?

答:關鍵在於對於 nums[mid] == target 這種情況的處理:

    if (nums[mid] == target)
        right = mid;

可見,找到 target 時不要立即返回,而是縮小「搜索區間」的上界 right,在區間 [left, mid) 中繼續搜索,即不斷向左收縮,達到鎖定左側邊界的目的。

5. 為什麼返回 left 而不是 right?

答:返回left和right都是一樣的,因為 while 終止的條件是 left == right。

四、尋找右側邊界的二分查找

尋找右側邊界和尋找左側邊界的代碼差不多,只有兩處不同,已標註:

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;

    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return left - 1; // 注意

1. 為什麼這個算法能夠找到右側邊界?

答:類似地,關鍵點還是這裏:

    if (nums[mid] == target) {
        left = mid + 1;

當 nums[mid] == target 時,不要立即返回,而是增大「搜索區間」的下界 left,使得區間不斷向右收縮,達到鎖定右側邊界的目的。

2. 為什麼最後返回 left – 1 而不像左側邊界的函數,返回 left?而且我覺得這裏既然是搜索右側邊界,應該返回 right 才對。

答:首先,while 循環的終止條件是 left == right,所以 left 和 right 是一樣的,你非要體現右側的特點,返回 right – 1 好了。

至於為什麼要減一,這是搜索右側邊界的一個特殊點,關鍵在這個條件判斷:

    if (nums[mid] == target) {
        left = mid + 1;
        // 這樣想: mid = left - 1

因為我們對 left 的更新必須是 left = mid + 1,就是說 while 循環結束時,nums[left] 一定不等於 target 了,而 nums[left – 1]可能是target。

至於為什麼 left 的更新必須是 left = mid + 1,同左側邊界搜索,就不再贅述。

3. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:類似之前的左側邊界搜索,因為 while 的終止條件是 left == right,就是說 left 的取值範圍是 [0, nums.length],所以可以添加兩行代碼,正確地返回 -1:

while (left < right) {
    // ...
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;

五、最後總結

先來梳理一下這些細節差異的因果邏輯:

第一個,最基本的二分查找算法:

因為我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因為我們只需找到一個 target 的索引即可
所以當 nums[mid] == target 時可以立即返回

第二個,尋找左側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最左側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊右側邊界以鎖定左側邊界

第三個,尋找右側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界

又因為收緊左側邊界時必須 left = mid + 1
所以最後無論返回 left 還是 right,必須減一

如果以上內容你都能理解,那麼恭喜你,二分查找算法的細節不過如此。

通過本文,你學會了:

1. 分析二分查找代碼時,不要出現 else,全部展開成 else if 方便理解。

2. 注意「搜索區間」和 while 的終止條件,如果存在漏掉的元素,記得在最後檢查。

3. 如需要搜索左右邊界,只要在 nums[mid] == target 時做修改即可。搜索右側時需要減一。

就算遇到其他的二分查找變形,運用這幾點技巧,也能保證你寫出正確的代碼。LeetCode Explore 中有二分查找的專項練習,其中提供了三種不同的代碼模板,現在你再去看看,很容易就知道這幾個模板的實現原理了。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

用象棋的思維趣說IT人的職業發展和錢途,在上海軟件行業,我見到的年入50萬的眾生相——我們該如何努力從而達到這個目標

    最近我花了不少功夫在學習象棋,也學習了王天一等高手的棋路,感覺IT人的職業和下棋一樣,往好了講,爭主動權爭實惠只爭朝夕,往壞了講,一步走錯得用多步來彌補,如果錯誤太大未必能彌補回來。在本文里,就用下棋這種比較輕鬆的話題來聊聊程序員該如何經營自己的職業。

1 對手是誰,如何算輸贏

    做任何事情都要明確對象和目標,下棋時,對手和目標都很明確,但在職業規劃里,對手是誰?如何算輸贏?

    對手從小了講,是自己,畢竟除山中賊易,除心中賊難,往大了講,是其它IT人。

    如何算輸贏?這有很多標準,往大了講,達到財務自由或者“掙到一個億”算贏,往中了講,有房有車有孩子,或者達到中產水平,往小了講,能在社會上立足,收入在平均水平以上。在本文里,不說過於遠大的目標,倒也不是遙不可及,而是再遠大的目標也是得靠腳踏實地,也不說“收入超平均水平”,畢竟這還是比較容易的,就算“達到中產水平”為贏。

    再具體點,如何算中產?我找了上海2018年中產的標準如下。

    1,有兩套及以上住房(不含和父母共有的),至少一套達非普標準;

    2,沒有房貸,或各項負債合計低於總資產30%。

    3,家庭年收入(不含父母)達50萬以上。

    之前本人寫過篇博文,在上海軟件行業,我見到的年入50萬的眾生相——我們該如何努力從而達到這個目標,年入50萬也算是中產吧。 

2 少走緩手棋

    一盤象棋有60個回合不算長,算下從大學畢業到退休,大致有30年吧,大概平攤下來,半年時間能算一個回合,也就是說,用半年的努力,自己職業上應該有顯著的效果。

    下棋時不怕局勢一般,就怕下緩手棋,高手之間過招,2步緩手足以輸棋,一般的人下棋,也經不起3步以上的緩手。

    那麼職業乃至人生的棋盤上,什麼叫緩手呢?

    1 在公司里得過且過,不主動追求技術進步,導致半年左右技術等方面沒進步。

    2 自己的技能已經得到提升,但出於安逸,沒有追求更高的工資或者更高級的職位。

    3 當工資收入達到一定水準后,不思進取,不積極探索新的掙錢渠道和掙錢模式。

    那麼在工作中,一旦下緩手棋會有什麼後果呢?

    1 半年不進步,看似沒什麼影響,但由奢入儉難,一旦得過且過的日子過慣了,再想上進就難了。

    2 如果一年不進步,技術馬上會落後於平均水平。

    3 一年半以上的不進步,就不說跳槽難了,這已經能導致在項目組裡日子難過(因為別人都在進步),更有甚者,如果公司動蕩,這批混日子而且技術不行的人,估計會第一批被淘汰,淘汰后甚至後面工作也難找。 

3 只爭朝夕,爭主動權更得靠效率

    和“少走緩手棋”相對的是“力爭主動權”。

    象棋里主動權直觀表現為,我可以從容調度子力進攻,能從容圍剿對方的子力從而得到子力優勢。在職業規劃里,主動權表現為,我不僅能從容應付當前的工作,而且由於我的技術以及能力在不斷提升,後繼我的技術發展方向和公司選型,我都掌握主動。從實惠角度來講,增加收入的主動權在自己手裡,在當前公司,我可以通過晉陞提升收入,我也可以通過跳槽提升收入,還可以通過各種額外手段提升收入。

    象棋里爭奪主動權靠積極和效率,所謂用最小的“度數”(棋子的移動步數)把子力放置到(給對方造成威脅最大的)積極位置。平時工作中,爭奪主動權的方式也是如此。

    怎麼算積極呢?列下我見到的比較積極的大牛事例。    

    1 態度端正,一天當两天用,在工作中,也絕無看網頁等做和工作無關事情的情況,有問題也是馬上確認積極解決,絕無拖沓。

    2 盡可能地利用各種時間碎片幹活或提升技術,比如我公司里的大牛中午休息時間也在看技術文章,博客園裡的一些大牛也是利用早上上班前或下班回家后的時間寫技術博客,總之積極地盡一切可能提升自己。

    3 從來不停止提升自己,比如我之前互聯網公司的一位大牛,Spring方面在業內非常出名,但平時也時一有時間就看Spring文章,一有可能就出書出視頻教程。

    而提升工作效率除了不斷總結之外,還得盡可能減少不必要的休閑娛樂時間,比如我之前互聯網公司的多位大牛,都是手機上沒有遊戲app,也沒聽說過追劇。

    知易行難,其實道理也很通俗,做起來並不容易,我的體會是,在開始階段稍微逼下自己,比如早起寫博客,工作時盡量不看無關事,平時積極些,可能最多一個月,當形成習慣時,想改也難。 

4 勢在子先,首先得提升自己的掙錢能力

    象棋里有寧失一子,不失一先的說法。如果把象棋中的形勢比作個人技能綜合能力(把握主動權的能力),那麼“子力”就好比掙到的錢。

    也就是說,能力比錢重要,一方面我們應該踏踏實實地提升能力,排除短期跳槽帶來的金錢誘惑,另一方面,在必要時,可以用錢買技能,比如在必要的時候買收費專欄里的知識,甚至參加培訓班。

    我見到的反面例子是,在之前的外企里,某人本身能力也就一般,但還在外面干兼職,這就屬於要錢不要技能,結果短時間內錢確實掙到了,但由於不注意提升能力,最後被迫離開了項目組。而正面的例子就比較多了,不少人在進項目組時能力一般,但平時通過看各種(免費和收費)資料,最後成為項目組內的棟樑。

    再說下應該重視哪些能力?

    1 對底層代碼的理解以及分析能力。

    2 能在短時間內全局掌握系統架構的能力。

    3 比較值錢的,比如大數據機器學習和分佈式的技能。

    4 帶團隊的能力,比如對外能和客戶方溝通,在公司內能和其它組協調扯皮,在組內能合理分派活,合理監管質量同時保證進度。 

5 優勢情況下更不能大意

   下棋時,我經常遇到如下的場景,我或者對手優勢很明顯,但由於一步走錯,局勢逆轉,對此象棋有“臨殺誤急“和“贏棋不鬧事”的說法。

    大家在工作中,這種情況也不能說沒,比如發布項目,什麼工作都準備好了,看似勝利在望,但在發布時,由於大意導致操作事務,最後出現產線問題, 

    象棋在沒贏前,任何走法都得反覆斟酌,同樣在發布項目等工作沒通過驗證前,也不能放鬆,這不僅得到主觀上態度端正,更得“未慮勝,先慮敗” ,盡量把可能導致失敗的因素都考慮全,並針對性地做預案或補償措施。

    我見過不少下棋的人,只會一廂情願,或者沉溺於自己的優勢中,或者主觀上忽視對方的反擊手段,這種人下棋贏了是運氣,輸了是必然,而且這樣下能力也不會提升。同樣,在做項目等工作時,寧可做好針對最壞情況的準備,比如發布時做好,甚至操練好回退預案,寧可用不到。這樣的話,一方面能確保勝局,另一方由於考慮周全,技能上一定也會有提升。 

6 取勝得靠進攻手段,IT人掙錢也得積極

    象棋中取勝一般有兩種方法,一種是確保自己不犯錯然後坐等對方走錯,另一種是積極主動擴大主動權,積優勢為勝勢。

    對應IT人掙錢方式,一種是干好手頭的活,確保不出問題,然後循例升職和加工資,另一種則是積極擴大自己的品牌,不斷探索新的掙錢方式,相比之下,後者更加積極,掙錢效率應該也優於前者。

    正如特級大師的擴大先手和進攻手段得靠慢慢積累,IT人掙錢的方式也得從小做起。比如小到通過寫博客積累,然後可以開專欄出視頻教程,再大點可以通過各種渠道找些項目,再大些可以做自己得產品。

    說起來容易做起來難,但一旦有積極想法了,而不是坐等工資等穩定收入,其它不敢說,至少自己的格局能不斷變廣,掙錢的機會也會越來越多。 

7 逆勢中不能自暴自棄,只要努力,就有翻盤可能

    下棋時不可能老是贏,有不少時候局面會很難看,只能苦苦支撐。相比之下,工作中也不可能一帆風順,比如工作氛圍不好比較壓抑,有996工作壓力大,或者錢少,這種時候絕不能自暴自棄。

    特級大師之間,出現翻盤的案例也不少,更何況業餘的,比如有10盤逆勢棋,如果自暴自棄亂下,估計最多一兩盤能下翻盤,但如果咬牙堅持每步都下好,翻盤的可能性就大很多。

    對應的在工作中,哪怕情況再差,每天也得儘力做好自己的事,必要時更得提升自己的能力,指不定哪天就時來運轉了,或者這個公司沒機會,由於自己一刻也沒放鬆反而在不斷提升自己的實力,在下家公司里就能春風得意了。

8 總結:人生棋局裡,一時輸贏不能代表今後的輸贏

     大家經常能看到,某人前幾年由於混日子,活得沒指望,但突然想明白了,只用了半年就完成了逆轉,也就是說,雖然一步走錯需要用更大的代價來彌補,但一時的輸贏絕不能代表一世的得失。所以,在任何時候都得積極向上力爭主動。本文也算戲說,大家在一笑之餘如果感到有意思,或者有收穫,請幫忙推薦下本文,謝謝大家。

版權說明:

    有不少網友轉載和想要轉載我的博文,本人感到十分榮幸,這也是本人不斷寫博文的動力。關於本文的版權有如下統一的說明,抱歉就不逐一回復了。

    1 本文可轉載,無需告知,轉載時請用鏈接的方式,給出原文出處,別簡單地通過文本方式給出,同時寫明原作者是hsm_computer。

    2 在轉載時,請原文轉載 ,謝絕洗稿。否則本人保留追究法律責任的權利。

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

附004.Kubernetes Dashboard簡介及使用

一 Kubernetes dashboard簡介

1.1 Web UI簡介

dashboard是基於Web的Kubernetes用戶界面。可以使用dashboard將容器化應用程序部署到Kubernetes集群,對容器化應用程序進行故障排除,以及管理集群資源。可以使用dashboard來概述群集上運行的應用程序,以及創建或修改單個Kubernetes資源(例如部署、任務、守護進程等)。可以使用部署嚮導擴展部署,啟動滾動更新,重新啟動Pod或部署新應用程序。
dashboard還提供有關群集中Kubernetes資源狀態以及可能發生的任何錯誤的信息。

二 dashboard部署

2.1 下載yaml

  1 [root@master ~]# mkdir dashboard					#建議將yaml存儲到本地
  2 [root@master ~]# cd dashboard/
  3 [root@master dashboard]# wget https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml

 

2.2 修改為國內源

  1 [root@master ~]# cd dashboard/
  2 [root@master dashboard]# vi kubernetes-dashboard.yaml
  3 ……
  4 image: mirrorgooglecontainers/kubernetes-dashboard-amd64:v1.10.1
  5 ……

 
提示:將yaml文件中的image字段修改為mirrorgooglecontainers/kubernetes-dashboard-amd64:v1.10.1。

2.3 安裝

  1 [root@master dashboard]# kubectl apply -f kubernetes-dashboard.yaml
  2 [root@master ~]# kubectl get pod --all-namespaces -o wide| grep kubernetes-dashboard			#確認驗證
  3 kube-system	kubernetes-dashboard-68ddcc97fc-c5thv	0/1	Running	0	30s	<none>	node2	<none>	<none>

 

三 dashboard訪問方式

3.1 訪問方式概述

安裝dashboard后,需要為用戶配置對群集資源的訪問控制。從版本1.7開始,dashboard不再具有默認授予的完全管理員權限。默認所有權限都被撤銷,並且只授予了使Dashboard工作所需的最小權限。
提示:本說明僅針對使用Dashboard 1.7及更高版本的用戶。如果確信dashboard需要授予管理員權限,可參考:《附006.Kubernetes身份認證》
通常,其他應用程序不應直接訪問dashboard。
dashboard有以下三種訪問方式:
kubectl proxy:只能在localhost上訪問。訪問地址:http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
NodePort:編輯 kubernetes-dashboard.yaml文件中,將 type: ClusterIP 改為 type: NodePort,確認dashboard運行在哪個節點后。訪問地址:https://<node-ip>:<nodePort>
apiserver:需要在瀏覽器中安裝用戶證書。訪問地址: https://<master-ip>:<apiserver-port>/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
提示:kubectl proxy方式不推薦,建議使用有效證書來建立安全的HTTPS連接。

3.2 kubectl proxy

  1 [root@master ~]# kubectl proxy
  2 [root@master ~]# curl http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/	#訪問

 
提示:建議通過後端形式,並且允許所有主機訪問的方式:

  1 [root@master ~]# nohup kubectl proxy --address='0.0.0.0' --accept-hosts='^*$' &

3.3 NodePort

NodePort訪問儀錶板的方式僅建議用於單節點設置中的Kubernetes環境。

  1 [root@master ~]# kubectl -n kube-system edit service kubernetes-dashboard
  2 ……
  3    type: NodePort
  4 ……
  5 #將type: ClusterIP  修改為 type: NodePort。

 
提示:以上操作也可通過以下命令一步完成:

  1 [root@master ~]# kubectl get pods --namespace=kube-system | grep dashboard
  2 kubernetes-dashboard-68ddcc97fc-c5thv   1/1     Running   0          3h14m
  3 [root@master ~]# kubectl describe pod kubernetes-dashboard-68ddcc97fc-c5thv --namespace=kube-system | grep Node
  4 Node:               node2/172.24.8.73

 
測試訪問:
瀏覽器訪問:http://172.24.8.73:30343/
提示:若部署的Kubernetes集群為多節點集群,需要通過以上方式查找dashboard所在的node節點,若為單節點集群,則直接訪問http://<master>:<port>即可。

3.4 apiserver

若Kubernetes API服務器公開並可從外部訪問,可瀏覽器直接訪問:https://172.24.8.71:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
注意:apiserver默認情況下使用system:anonymous用戶進行訪問,因此沒有權限打開相應資源。只有在選擇在瀏覽器中安裝用戶證書時,才能使用這種訪​​問儀錶板的方式。
NodePort和apiserver都需要配置相應的認證才可訪問,確定某種方式方式后需要配置認證類型。

3.5 Ingress

dashboard也可以使用ingress進行資源公開。
可參考:https://kubernetes.io/docs/concepts/services-networking/ingress/

四 dashboard驗證方式

瀏覽器訪問的時候需要加載相應證書,部署完成后默認已經創建相應證書,可參考附01進行導出。由於Kubernetes默認證書可能過期導致無法訪問dashboard,本實驗在已成功部署Kubernetes後手動重新創建證書。

4.1 創建證書

  1 [root@master ~]# mkdir /etc/kubernetes/dash_pki
  2 [root@master ~]# cd /etc/kubernetes/dash_pki/
  3 [root@master dash_pki]# openssl genrsa -out ca.key 2048				#生成一個 2048 bit 的 ca.key
  4 [root@master dash_pki]# openssl req -x509 -new -nodes -key ca.key -subj "/CN=172.24.8.71" -days 10000 -out ca.crt	                #根據 ca.key 生成一個 ca.crt(使用 -days 設置證書的有效時間)
  5 [root@master dash_pki]# openssl genrsa -out server.key 2048			        #生成一個 2048 bit 的 server.key
  6 [root@master dash_pki]# openssl req -new -key server.key -subj "/CN=172.24.8.71" -out server.csr			                #根據 server.key 生成一個 server.csr
  7 [root@master dash_pki]# openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 10000	#根據 ca.key、ca.crt 和 server.csr 生成 server.crt
  8 subject=/CN=172.24.8.71
  9 Getting CA Private Key
 10 [root@master dash_pki]# openssl x509  -noout -text -in ./server.crt		        #查看證書

 

4.2 修改默認證書配置

  1 [root@master ~]# cd dashboard/
  2 [root@master dashboard]# kubectl delete -f kubernetes-dashboard.yaml		#刪除使用默認證書所創建的dashboard
  3 [root@master dashboard]# ll /etc/kubernetes/dash_pki/
  4 [root@master dashboard]# kubectl create secret generic kubernetes-dashboard-certs --from-file="/etc/kubernetes/dash_pki/server.crt,/etc/kubernetes/dash_pki/server.key" -n kube-system	#掛載新證書到dashboard
  5 [root@master dashboard]# kubectl get secret kubernetes-dashboard-certs -n kube-system -o yaml	#查看新證書

 

4.3 重新部署dashboard

  1 [root@master dashboard]# kubectl apply -f kubernetes-dashboard.yaml
  2 [root@master dashboard]# kubectl get pods --namespace=kube-system | grep dashboard		#確認驗證

 

4.4 導入證書

將server.crt導入IE瀏覽器,導入操作略。

4.5 訪問測試

本試驗基於apiserver訪問方式+Kubeconfig身份驗證進行登錄。
通過apiserver形式訪問:https://172.24.8.71:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
提示:dashboard登錄整個流程可參考:https://www.cnadn.net/post/2613.htm
提示:apiserver方式見3.4,Kubeconfig驗證方式見《附006.Kubernetes身份認證》中的3.5。  

附001:導出當前Kubernetes證書

[root@master ~]# grep ‘client-certificate-data’ ~/.kube/config | head -n 1 | awk ‘{print $2}’ | base64 -d >> kubecfg.crt
[root@master ~]# grep ‘client-key-data’ ~/.kube/config | head -n 1 | awk ‘{print $2}’ | base64 -d >> kubecfg.key
[root@master ~]# openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out k8s.crt -name “kubernetes-client”
Enter Export Password:[x120952576]
Verifying – Enter Export Password:[x120952576]
使用相應的密碼,將k8s.crt導入IE瀏覽器,導入操作略。    
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=20stclch1nfo4

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

從零開始寫一個Exporter

前言

上一篇文章中已經給大家整體的介紹了開源監控系統Prometheus,其中Exporter作為整個系統的Agent端,通過HTTP接口暴露需要監控的數據。那麼如何將用戶指標通過Exporter的形式暴露出來呢?比如說在線,請求失敗數,異常請求等指標可以通過Exporter的形式暴露出來,從而基於這些指標做告警監控。

 

演示環境

$ uname -a
Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64
$ go version
go version go1.12.4 darwin/amd64

 

四類指標介紹

Prometheus定義了4種不同的指標類型:Counter(計數器),Gauge(儀錶盤),Histogram(直方圖),Summary(摘要)。

其中Exporter返回的樣本數據中會包含數據類型的說明,例如:

# TYPE node_network_carrier_changes_total counter
node_network_carrier_changes_total{device="br-01520cb4f523"} 1

這四類指標的特徵為:

Counter:只增不減(除非系統發生重啟,或者用戶進程有異常)的計數器。常見的監控指標如http_requests_total, node_cpu都是Counter類型的監控指標。一般推薦在定義為Counter的指標末尾加上_total作為後綴。

Gauge:可增可減的儀錶盤。Gauge類型的指標側重於反應系統當前的狀態。因此此類指標的數據可增可減。常見的例如node_memory_MemAvailable_bytes(可用內存)。

HIstogram:分析數據分佈的直方圖。显示數據的區間分佈。例如統計請求耗時在0-10ms的請求數量和10ms-20ms的請求數量分佈。

Summary: 分析數據分佈的摘要。显示數據的中位數,9分數等。

 

實戰

接下來我將用Prometheus提供的Golang SDK 編寫包含上述四類指標的Exporter,示例的編寫修改自SDK的example。由於example中示例比較複雜,我會精簡一下,盡量讓大家用最小的學習成本能夠領悟到Exporter開發的精髓。第一個例子會演示Counter和Gauge的用法,第二個例子演示Histogram和Summary的用法。

Counter和Gauge用法演示:

package main

import (
    "flag"
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")

func main() {
    flag.Parse()
    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(*addr, nil))
}

上述代碼就是一個通過0.0.0.0:8080/metrics 暴露golang信息的原始Exporter,沒有包含任何的用戶自定義指標信息。接下來往裡面添加Counter和Gauge類型指標:

 1 func recordMetrics() {
 2     go func() {
 3         for {
 4             opsProcessed.Inc()
 5             myGague.Add(11)
 6             time.Sleep(2 * time.Second)
 7         }
 8     }()
 9 }
10 
11 var (
12     opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
13         Name: "myapp_processed_ops_total",
14         Help: "The total number of processed events",
15     })
16     myGague = promauto.NewGauge(prometheus.GaugeOpts{
17         Name: "my_example_gauge_data",
18         Help: "my example gauge data",
19         ConstLabels:map[string]string{"error":""},
20     })
21 )

在上面的main函數中添加recordMetrics方法調用。curl 127.0.0.1:8080/metrics 能看到自定義的Counter類型指標myapp_processed_ops_total 和 Gauge 類型指標my_example_gauge_data。

# HELP my_example_gauge_data my example gauge data
# TYPE my_example_gauge_data gauge
my_example_gauge_data{error=""} 44
# HELP myapp_processed_ops_total The total number of processed events
# TYPE myapp_processed_ops_total counter
myapp_processed_ops_total 4

其中#HELP 是代碼中的Help字段信息,#TYPE 說明字段的類型,例如my_example_gauge_data是gauge類型指標。my_example_gauge_data是指標名稱,大括號括起來的error是該指標的維度,44是該指標的值。需要特別注意的是第12行和16行用的是promauto包的NewXXX方法,例如:

func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
    c := prometheus.NewCounter(opts)
    prometheus.MustRegister(c)
    return c
}

可以看到該函數是會自動調用MustRegister方法,如果用的是prometheus包的NewCounter則需要再自行調用MustRegister註冊收集的指標。其中Couter類型指標有以下的內置接口:

type Counter interface {
    Metric
    Collector

    // Inc increments the counter by 1. Use Add to increment it by arbitrary
    // non-negative values.
    Inc()
    // Add adds the given value to the counter. It panics if the value is <
    // 0.
    Add(float64)
}

可以通過Inc()接口給指標直接進行+1操作,也可以通過Add(float64)給指標加上某個值。還有繼承自Metric和Collector的一些描述接口,這裏不做展開。

Gauge類型的內置接口有:

type Gauge interface {
    Metric
    Collector

    // Set sets the Gauge to an arbitrary value.
    Set(float64)
    // Inc increments the Gauge by 1. Use Add to increment it by arbitrary
    // values.
    Inc()
    // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary
    // values.
    Dec()
    // Add adds the given value to the Gauge. (The value can be negative,
    // resulting in a decrease of the Gauge.)
    Add(float64)
    // Sub subtracts the given value from the Gauge. (The value can be
    // negative, resulting in an increase of the Gauge.)
    Sub(float64)

    // SetToCurrentTime sets the Gauge to the current Unix time in seconds.
    SetToCurrentTime()
}

需要注意的是Gauge提供了Sub(float64)的減操作接口,因為Gauge是可增可減的指標。Counter因為是只增不減的指標,所以只有加的接口。

 

Histogram和Summary用法演示:

 1 package main
 2 
 3 import (
 4     "flag"
 5     "fmt"
 6     "log"
 7     "math"
 8     "math/rand"
 9     "net/http"
10     "time"
11 
12     "github.com/prometheus/client_golang/prometheus"
13     "github.com/prometheus/client_golang/prometheus/promhttp"
14 )
15 
16 var (
17     addr              = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")
18     uniformDomain     = flag.Float64("uniform.domain", 0.0002, "The domain for the uniform distribution.")
19     normDomain        = flag.Float64("normal.domain", 0.0002, "The domain for the normal distribution.")
20     normMean          = flag.Float64("normal.mean", 0.00001, "The mean for the normal distribution.")
21     oscillationPeriod = flag.Duration("oscillation-period", 10*time.Minute, "The duration of the rate oscillation period.")
22 )
23 
24 var (
25     rpcDurations = prometheus.NewSummaryVec(
26         prometheus.SummaryOpts{
27             Name:       "rpc_durations_seconds",
28             Help:       "RPC latency distributions.",
29             Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
30         },
31         []string{"service","error_code"},
32     )
33     rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
34         Name:    "rpc_durations_histogram_seconds",
35         Help:    "RPC latency distributions.",
36         Buckets: prometheus.LinearBuckets(0, 5, 20),
37     })
38 )
39 
40 func init() {
41     // Register the summary and the histogram with Prometheus's default registry.
42     prometheus.MustRegister(rpcDurations)
43     prometheus.MustRegister(rpcDurationsHistogram)
44     // Add Go module build info.
45     prometheus.MustRegister(prometheus.NewBuildInfoCollector())
46 }
47 
48 func main() {
49     flag.Parse()
50 
51     start := time.Now()
52 
53     oscillationFactor := func() float64 {
54         return 2 + math.Sin(math.Sin(2*math.Pi*float64(time.Since(start))/float64(*oscillationPeriod)))
55     }
56 
57     go func() {
58         i := 1
59         for {
60             time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond)
61             if (i*3) > 100 {
62                 break
63             }
64             rpcDurations.WithLabelValues("normal","400").Observe(float64((i*3)%100))
65             rpcDurationsHistogram.Observe(float64((i*3)%100))
66             fmt.Println(float64((i*3)%100), " i=", i)
67             i++
68         }
69     }()
70 
71     go func() {
72         for {
73             v := rand.ExpFloat64() / 1e6
74             rpcDurations.WithLabelValues("exponential", "303").Observe(v)
75             time.Sleep(time.Duration(50*oscillationFactor()) * time.Millisecond)
76         }
77     }()
78 
79     // Expose the registered metrics via HTTP.
80     http.Handle("/metrics", promhttp.Handler())
81     log.Fatal(http.ListenAndServe(*addr, nil))
82 }

第25-32行定義了一個Summary類型指標,其中有service和errro_code兩個維度。第33-37行定義了一個Histogram類型指標,從0開始,5為寬度,有20個直方。也就是0-5,6-10,11-15 …. 等20個範圍統計。

其中直方圖HIstogram指標的相關結果為:

 1 # HELP rpc_durations_histogram_seconds RPC latency distributions.
 2 # TYPE rpc_durations_histogram_seconds histogram
 3 rpc_durations_histogram_seconds_bucket{le="0"} 0
 4 rpc_durations_histogram_seconds_bucket{le="5"} 1
 5 rpc_durations_histogram_seconds_bucket{le="10"} 3
 6 rpc_durations_histogram_seconds_bucket{le="15"} 5
 7 rpc_durations_histogram_seconds_bucket{le="20"} 6
 8 rpc_durations_histogram_seconds_bucket{le="25"} 8
 9 rpc_durations_histogram_seconds_bucket{le="30"} 10
10 rpc_durations_histogram_seconds_bucket{le="35"} 11
11 rpc_durations_histogram_seconds_bucket{le="40"} 13
12 rpc_durations_histogram_seconds_bucket{le="45"} 15
13 rpc_durations_histogram_seconds_bucket{le="50"} 16
14 rpc_durations_histogram_seconds_bucket{le="55"} 18
15 rpc_durations_histogram_seconds_bucket{le="60"} 20
16 rpc_durations_histogram_seconds_bucket{le="65"} 21
17 rpc_durations_histogram_seconds_bucket{le="70"} 23
18 rpc_durations_histogram_seconds_bucket{le="75"} 25
19 rpc_durations_histogram_seconds_bucket{le="80"} 26
20 rpc_durations_histogram_seconds_bucket{le="85"} 28
21 rpc_durations_histogram_seconds_bucket{le="90"} 30
22 rpc_durations_histogram_seconds_bucket{le="95"} 31
23 rpc_durations_histogram_seconds_bucket{le="+Inf"} 33
24 rpc_durations_histogram_seconds_sum 1683
25 rpc_durations_histogram_seconds_count 33

xxx_count反應當前指標的記錄總數,xxx_sum表示當前指標的總數。不同的le表示不同的區間,後面的数字是從開始到這個區間的總數。例如le=”30″後面的10表示有10個樣本落在0-30區間,那麼26-30這個區間一共有多少個樣本呢,只需要用len=”30″ – len=”25″,即2個。也就是27和30這兩個點。

Summary相關的結果如下:

 1 # HELP rpc_durations_seconds RPC latency distributions.
 2 # TYPE rpc_durations_seconds summary
 3 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.5"} 7.176288428497417e-07
 4 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.9"} 2.6582266087185467e-06
 5 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.99"} 4.013935374172691e-06
 6 rpc_durations_seconds_sum{error_code="303",service="exponential"} 0.00015065426336339398
 7 rpc_durations_seconds_count{error_code="303",service="exponential"} 146
 8 rpc_durations_seconds{error_code="400",service="normal",quantile="0.5"} 51
 9 rpc_durations_seconds{error_code="400",service="normal",quantile="0.9"} 90
10 rpc_durations_seconds{error_code="400",service="normal",quantile="0.99"} 99
11 rpc_durations_seconds_sum{error_code="400",service="normal"} 1683
12 rpc_durations_seconds_count{error_code="400",service="normal"} 33

其中sum和count指標的含義和上面Histogram一致。拿第8-10行指標來說明,第8行的quantile 0.5 表示這裏指標的中位數是51,9分數是90。

 

自定義類型

如果上面Counter,Gauge,Histogram,Summary四種內置指標都不能滿足我們要求時,我們還可以自定義類型。只要實現了Collect接口的方法,然後調用MustRegister即可:

func MustRegister(cs ...Collector) {
    DefaultRegisterer.MustRegister(cs...)
}

type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

 

總結

文章通過Prometheus內置的Counter(計數器),Gauge(儀錶盤),Histogram(直方圖),Summary(摘要)演示了Exporter的開發,最後提供了自定義類型的實現方法。

 

參考

https://prometheus.io/docs/guides/go-application/

https://yunlzheng.gitbook.io/prometheus-book/parti-prometheus-ji-chu/promql/prometheus-metrics-types

https://songjiayang.gitbooks.io/prometheus/content/concepts/metric-types.html

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

20年研發管理經驗談(十一)

本文繼20年研發管理經驗談(十)。

此文是對我個人測試思想的一個總結,由於經驗不夠,知識淺薄,如果有什麼不合理的地方請一笑了之。

一、面向對象的概念
  所謂的面向對象是軟件開發的一種重要的思維方式,是把軟件開發過程中出現的事物,用一個個的對像來分析.一般一張數據表可以封裝為一個對像。用個形象的比喻:我們現在要做一張桌子,首先我們考慮到的是我們要做的是什麼?是桌子;桌子是用來干什麼的呢?是用來吃飯、喝茶、看書、打麻將的;然後就要考慮桌子由哪些部分組成?由桌面和桌腿來組成;接着我們需要考慮我們採用什麼材料呢?紙?不行…那可什麼都幹不成,OK,用木頭;接着就可以開始把組成桌子的組件做為對象開始分析–桌面如何做是用刀砍的還是用刨子刨呢?桌腿又如何做…
  一套完整的方法成形了就可以具體實現了,在做的過程中桌面要做多大,桌腿要做多長都要事先考慮到是不是要留出接口,這些就是我們給組成桌子的組件賦予的屬性。OK,現在可以做出具體的實物了,做好實物組件(對象)以後就要將做好的桌面桌腿進行組裝,由於我們事先考慮好了組件的屬性,考慮到了必須預留接口,因此我們可以很輕易的組合成功,桌子做出來了。以上就是面向對象的思想的一個簡要的比喻

  了解面向對象必須了解的幾個名詞:對象、方法、屬性、繼承、多態。

 

二、遊戲測試
  遊戲測試是整個軟件測試行業中比較特殊的一部份,他有着大多數軟件測試的共性,也具備自身的特性,而相對於許多通用軟件的測試來說,遊戲測試所具備的特性是非常明顯的。現在就簡要的說說上面提到的共性和特性。
共性:
1、測試的目的就是為了盡可能的發現軟件存在和潛在的問題。
2、測試都是需要測試人員按照產品行為描述來實施。產品行為描述可以是書面的規格說明書、需求文檔、產品文件、或是用戶手冊、源代碼、或是工作的可執行程序。
3、每一種測試都需要產品運行於真實的或是模擬環境之下。
4、每一種測試都要求以系統方法展示產品功能, 以證明測試結果是否有效, 以及發現其中出錯的原因, 從而讓程序人員進行改進。
總之,軟件測試就是對產品進行盡可能的全面檢查,盡可能的發掘bug,提高軟件質量,從而為企業創造利潤。

特性:
  網絡遊戲世界從某種意義上說是另一個人類社會,只是人們在網絡遊戲世界中進行着在被允許的範圍內的活動,包括了修鍊、交流、合作、經商、欺詐、情感、衝突等等。而在遊戲製作時這些進行這些行為的部分就是一個個完整的功能,我們在進行測試的時候,需要考慮的不僅僅是能否實現功能,要考慮更多的是人們在進行操作時會如何做,可能有多少種做法,這些做法應該有什麼樣的響應,哪些做法是被禁止的,在進行了被禁止的操作后應該有什麼的響應。因此這裏就是涉及到了遊戲世界的測試方法:
1、遊戲情節的測試,主要指遊戲世界中的任務系統的組成, 有人也稱為遊戲世界的事件驅動, 我喜歡稱為遊戲情感世界的測試。
2、遊戲世界的平衡測試,主要表現在經濟平衡,能力平衡( 包含技能, 屬性等等),保證遊戲世界競爭公平。
3、遊戲文化的測試,比如整個遊戲世界的風格, 是中國文化主導,還是日韓風格等等,大到遊戲整體,小到N P C( 遊戲世界人物) 對話, 比如一個書生,他的對話就必需斯文, 不可以用江湖語言。

以上陳述中關於遊戲特性的部分概念是曾在金山公司的測試人陳衛俊提出來過的,在此引用。

 

三、如何用面向對象的思想進行測試
  上面了解了面向對象的概念以及遊戲測試和通用軟件測試的區別以後我們可以進入正題了—如何用面向對象的思想進行遊戲測試?
  首先,和所有通用軟件以及硬件產品一樣,我們的遊戲是一個產品,是一個存在的實體,因此,我們把這個”實體”當做一個大的對象開始分析,整個遊戲由哪些部分構成,而構成整個遊戲的大的部分又由哪些組件構成,認真分析完這些以後就可以着手進行測試了,注意,這裏說”可以進行測試了”意思不是馬上就能進入測試,聽我慢慢道來。 
  ”工欲善其事,必先利其器”—某位高人說的,我們做測試也是一樣,分析完畢后,我們要做的還是分析 ^_^ 不過這裏的分析和之前的分析有點點區別,這裏我們需要分析的是具體功能的關鍵測試點和風險點,測試不能盲目,打蛇要打七寸…..在這裏我們就是把某個具體的功能作為一個對象,我們要分析組成這個功能的是哪些因素,一共有哪些測試點,哪些測試點是關鍵點,哪些是高風險點,一一列舉出來,這樣我們就一目瞭然了,然後就是我們打算採用何種方式來進行測試,這裏就是方法了.測試的方式可能有很多種(比如在不同的操作系統下進行測試等),因此我們也需要一一列舉,此外我們需要分析的還有測試過程中我們需要用到的具體測試手法、具體的數值、特定的環境等等這些就是屬性,當然這些我們也必須整理出來。
  將以上提到的對象、方法、屬性整理成文檔就是我們測試時所必須的測試用例了。當然,還是老話,測試用例的優劣是以覆蓋面來評判的,這裏就需要經驗了,簡單說就是靠累積以及學習。
  OK,測試用例我們完成了,剩下的就是實施測試了,實施測試時個人覺得一定要按照用例的描述去執行,如果在測試過程中覺得用例不完善可以先更新用例再進行測試,一定不要先測試再補用例!!
  接下來就是測試報告,報告中包含的應該有所有測試點的簡述,包括了通過測試的部分和存在bug的部分。bug管理是很重要的一環,在這裏不詳述。
  關於測試流程在這裏就不做具體說明,在這裏希望闡述的是一種測試的思想,個人覺得測試除了要有紮實的相關基礎知識以便更深入的了解產品以外,更重要的是測試思想,具備了完善的測試思想才能有計劃的完成每一步測試,從而提高測試的效率,保證測試產出的質量,也更好的保證產品的質量。面向對象是一種思想,用面向對象的思想來組織、計劃、實施測試工作,能讓我們在測試工作中有很強的目的性,他能清楚地告訴我們今天要做什麼,明天要做什麼,我們要做的是哪些,說回遊戲測試,遊戲開發是一個迭帶的開發模式,因此測試工作往往會有很大的隨機性,因此當我們接到一個新功能時,首先要明確我們要測得這個功能是做什麼的,有什麼用,這個功能怎麼使用。OK,我們了解了這個功能是什麼,能做什麼就可以開始細化分析了:這個功能共由哪些子功能組成,這些子功能是否有自己的子功能點,一層層的分析下去,然後就是從最底層的功能點分析:這個功能什麼情況下要發揮其功效,發揮其功效的因素有哪些,怎麼樣去發揮具體的功效,該功能有沒有相應的容錯機制,這些就是我們的詳細測試點和測試手法。然後向上一層一層分析,一直到最頂層就是我們的功能完整的測試方針。這樣我們就把面向對象的思想完全用到了測試中。當然,在分析的過程中我們必須考慮到,與遊戲情節、遊戲風格、遊戲平衡、玩家的易用性是否衝突等等因素,適時地給策劃提出正確的建議。

  以上陳述的種種,無非是想將面向對象的思想用到測試中的好處列舉出來,或許經驗淺薄說的有些蒼白,但是我堅信一點,測試是一種思想,是一種絕對不亞於開發思想的學問,要想做好測試就需要具備良好的測試思想,或者良好的測試思想不是一天两天能夠形成的但是相信只要把測試當做一種職業,當作一種事業來做,把自己真正當成保證產品質量的最後一道關卡,成為一個BT(BestTester)就指日可待了!

軟件測試用例的認識誤區

  軟件測試用例是為了有效發現軟件缺陷而編寫的包含測試目的、測試步驟、期望測試結果的特定集合。正確認識和設計軟件測試用例可以提高軟件測試的有效性,便於測試質量的度量,增強測試過程的可管理性。

  在實際軟件項目測試過程中,由於對軟件測試用例的作用和設計方法的理解不同,測試人員(特別是剛從事軟件測試的新人)對軟件測試用例存在不少錯誤的認識,給實際軟件測試帶來了負面影響,本文對這些認識誤區進行列舉和剖析。

 

 

誤區之一:測試輸入數據設計方法等同於測試用例設計方法

  現在一些測試書籍和文章中講到軟件測試用例的設計方法,經常有這樣的表述:測試用例的設計方法包括:等價類、邊界值、因果圖、錯誤推測法、場景設計法等。這種表述是很片面的,這些方法只是軟件功能測試用例設計中如何確定測試輸入數據的方法,而不是測試用例設計的全部內容。

  這種認識的不良影響可能會使不少人認為測試用例設計就是如何確定測試的輸入數據,從而掩蓋了測試用例設計內容的豐富性和技術的複雜性。如果測試用例設計人員把這種認識拿來要求自己,則害了自己;拿來教人,則害了別人;拿來指導測試,則害了測試團隊。聽起來似乎是“小題大做”,但是絕不是“危言聳聽”。

  無疑,對於軟件功能測試和性能測試,確定測試的輸入數據很重要,它決定了測試的有效性和測試的效率。但是,測試用例中輸入數據的確定方法只是測試用例設計方法的一個子集,除了確定測試輸入數據之外,測試用例的設計還包括如何根據測試需求、設計規格說明等文檔確定測試用例的設計策略、設計用例的表示方法和組織管理形式等問題。

  在設計測試用例時,需要綜合考慮被測軟件的功能、特性、組成元素、開發階段(里程碑)、測試用例組織方法(是否採用測試用例的數據庫管理)等內容。具體到設計每個測試用例而言,可以根據被測模塊的最小目標,確定測試用例的測試目標;根據用戶使用環境確定測試環境;根據被測軟件的複雜程度和測試用例執行人員的技能確定測試用例的步驟;根據軟件需求文檔和設計規格說明確定期望的測試用例執行結果。

 

 

誤區之二:強調測試用例設計得越詳細越好

  在確定測試用例設計目標時,一些項目管理人員強調測試用例“越詳細越好”。具體表現在兩個方面:盡可能設計足夠多的設計用例,測試用例的數量閱讀越好;測試用例盡可能包括測試執行的詳細步驟,達到“任何一個人都可以根據測試用例執行測試”,追求測試用例越詳細越好。

  這種做法和觀點最大的危害就是耗費了很多的測試用例設計時間和資源,可能等到測試用例設計、評審完成后,留給實際執行測試的時間所剩無幾了。因為當前軟件公司的項目團隊在規劃測試階段,分配給測試的時間和人力資源是有限的,而軟件項目的成功要堅持“質量、時間、成本”的最佳平衡,沒有足夠多的測試執行時間,就無法發現更多的軟件缺陷,測試質量更無從談起了。

  編寫測試用例的根本目的是有效地找出軟件可能存在的缺陷,為了達到這個目的,需要分析被測試軟件的特徵,運用有效的測試用例設計方法,盡量使用較少的測試用例,同時滿足合理的測試需求覆蓋,從而達到“少花時間多辦事”的效果。

  測試用例中的測試步驟需要詳細到什麼程度,主要取決於測試用例的“最終用戶”(即執行這些測試用例的人員),以及測試用例執行人員的技能和產品熟悉程度。如果編寫測試用例的人員也是測試用例執行人員,或者測試用例的執行人員深刻了解被測軟件,測試用例就沒有必要太詳細。而如果是測試新人執行測試用例,或者軟件測試外包給獨立的第三方公司,那麼測試用例的執行步驟最好足夠詳細。

 

誤區之三:追求測試用例設計“一步到位”

  現在軟件公司都意識到了測試用例設計的重要性了,但是一些人認為設計測試用例是一次性投入,測試用例設計一次就“萬事大吉”了,片面追求測試設計的“一步到位”。

這種認識造成的危害性使設計出的測試用例缺乏實用性,或者誤導測試用例執行人員,誤報很多不是軟件缺陷的“Bug”,這樣的測試用例在測試執行過程中“形同虛設”,難免淪為“垃圾文檔”的地步。

  “唯一不變的是變化”。任何軟件項目的開發過程都處於不斷變化過程中,用戶可能對軟件的功能提出新需求,設計規格說明相應地更新,軟件代碼不斷細化。設計軟件測試用例與軟件開發設計并行進行,必須根據軟件設計的變化,對軟件測試用例進行內容的調整,數量的增減,增加一些針對軟件新增功能的測試用例,刪除一些不再適用的測試用例,修改那些模塊代碼更新了的測試用例。

  軟件測試用例設計只是測試用例管理的一個過程,除此之外,還要對其進行評審、更新、維護,以便提高測試用例的“新鮮度”,保證“可用性”。因此,軟件測試用例也要堅持“與時俱進”的原則。

 

 

誤區之四:讓測試新人設計測試用例

  在與測試同行交流的過程中,不少剛參加測試工作的測試新人經常詢問的一個問題是:“怎麼才能設計好測試用例?”。因為他(她)們以前從來沒有設計過測試用例,面對大型的被測試軟件感到“老虎吃天,無從下口”。

  讓測試新人設計測試用例是一種高風險的測試組織方式,它帶來的不利後果是設計出的測試用例對軟件功能和特性的測試覆蓋性不高,編寫效率低,審查和修改時間長,可重用性差。

  軟件測試用例設計是軟件測試的中高級技能,不是每個人(尤其是測試新人)都可以編寫的,測試用例編寫者不僅要掌握軟件測試的技術和流程,而且要對被測軟件的設計、功能規格說明、用戶試用場景以及程序/模塊的結構都有比較透徹的理解。

  因此,實際測試過程中,通常安排經驗豐富的測試人員進行測試用例設計,測試新人可以從執行測試用例開始,隨着項目進度的不斷進展,測試人員的測試技術和對被測軟件的不斷熟悉,可以積累測試用例的設計經驗,編寫測試用例。

 

 

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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

kubernetes高級之集群中使用sysctls

系列目錄

在linux系統里,sysctls 接口允許管理員在運行時修改內核參數.參數存在於/proc/sys/虛擬進程文件系統里.參數涉及到很多子模塊,例如:

  • 內核(kernel)(常見前綴kernel.)

  • 網絡(networking)(常見前綴net.)

  • 虛擬內存(virtual memory) (常見前綴 vm.)

  • MDADM(常見前綴dev.)

啟用非安全sysctls

sysctls分為安全和非安全的.除了合理地劃分名稱空間外一個安全的sysctl必須在同一個節點上的pod間是隔離的.這就意味着為一個pod設置安全的sysctl需要考慮以下:

  • 必須不能影響同一節點上的其它pod

  • 必須不能危害節點的健康

  • 必須不能獲取自身pod所限制以外的cpu或內存資源

截至目前,大部分名稱空間下的sysctls都不被認為是安全的.以下列出被kubernetes安全支持:

  • kernel.shm_rmid_forced

  • net.ipv4.ip_local_port_range

  • net.ipv4.tcp_syncookies

如果日後kubelete支持更好的隔離機制,這份支持的安全列表將會擴展

所有安全sysctls默認被開啟

所有的非安全sysctls默認被關閉,管理員必須手動在pod級別啟動.包含非安全sysctls的pod仍然會被調度,但是將啟動失敗.

請牢記以上警告,集群管理員可以在特殊情況下,比如為了高性能或者時實應用系統優化,可以啟動相應的sysctls.sysctl可以通過kubelet在節點級別啟動

即需要在想要開啟sysctl的節點上手動啟動.如果要在多個節點上啟動則需要分別進入相應的節點進行設置.

kubelet --allowed-unsafe-sysctls \
  'kernel.msg*,net.ipv4.route.min_pmtu' ...

對於minikube,則可以通過extra-config來配置

minikube start --extra-config="kubelet.allowed-unsafe-sysctls=kernel.msg*,net.ipv4.route.min_pmtu"...

僅有名稱空間的sysctls可以通過這種方式開啟

為pod設置Sysctls

一系列的sysctls被劃分在不同的名稱空間內.這意味着他們可以為節點上的pod單獨地設置.僅有名稱空間的sysctls可以通過pod的securityContext被設置

以下列出的是已知的有名稱空間的.在日後的linux內核版本中可能會改變

  • kernel.shm*,

  • kernel.msg*,

  • kernel.sem,

  • fs.mqueue.*,

  • net.*.

沒有名稱空間的systls被稱作節點級別sysctls.如果你需要設置它們,你必須在每個節點的操作系統上手動設置,或者通過有特權的DaemonSet來設置

使用pod的安全上下文(securityContext)來設置有名稱空間的sysctls.安全上下文對pod內的所有容器都產生效果.

以下示例通過pod的安全上下文來設置一個安全的sysctl kernel.shm_rmid_forced和兩個非安全的sysctls net.ipv4.route.min_pmtu以及kernel.msgmax .在pod的spec裏面,安全的sysctl和非安全的sysctl聲明並沒有區別

在生產環境中,僅僅在你明白了要設置的sysctl的功能時候才進行設置,以免造成系統不穩定.

apiVersion: v1
kind: Pod
metadata:
  name: sysctl-example
spec:
  securityContext:
    sysctls:
    - name: kernel.shm_rmid_forced
      value: "0"
    - name: net.ipv4.route.min_pmtu
      value: "552"
    - name: kernel.msgmax
      value: "65536"
  ...

由於非安全sysctls的非安全特徵,設置非安全sysctls產生的後果將由你自行承擔,可能產生的後果包含pod行為異常,資源緊張或者節點完全崩潰

pod安全策略(PodSecurityPolicy)

你可以通過設置pod安全策略里的forbiddenSysctls(和)或者allowedUnsafeSysctls來進一步控制哪些sysctls可以被設置.一個以*結尾的sysctl,比如kernel.*匹配其下面所有的sysctl

forbiddenSysctlsallowedUnsafeSysctls均是一系列的純字符串sysctl名稱或者sysctl模板(以*結尾).*匹配所有的sysctl

forbiddenSysctls將排除一系列sysctl.你可以排除一系列安全和非安全的sysctls.如果想要禁止設置任何sysctls,可以使用*

如果你在allowedUnsafeSysctls字段設置了非安全sysctls,並且沒有出現在forbiddenSysctls字段里,則使用了此pod安全策略的pods可以使用這個(些)(sysctls).如果想啟用所有的非安全sysctls,可以設置*

警告,如果你通過pod安全策略的allowedUnsafeSysctls把非安全sysctl添加到白名單(即可以執行),但是如果節點級別沒有通過sysctl設置--allowed-unsafe-sysctls,pod將啟動失敗.

以下示例允許以kernel.msg開頭的sysctls被設置,但是禁止設置kernel.shm_rmid_forced

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: sysctl-psp
spec:
  allowedUnsafeSysctls:
  - kernel.msg*
  forbiddenSysctls:
  - kernel.shm_rmid_forced
 ...

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

SpringBoot啟動流程分析(六):IoC容器依賴注入

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

一、前言

  前面我們對IoC容器的初始化過程進行了詳細的分析,這個初始化過程完成的主要工作是在IoC容器中建立BeanDefinition數據映射。在此過程中並沒有看到IoC容器對Bean依賴關係進行注入,接下來分析一下IoC容器是怎樣對Bean的依賴關係進行注入的。

  前面在refresh()–>invokeBeanFactoryPostProcessors(beanFactory);方法中已經完成了IoC容器的初始化並已經載入了我們定義的Bean的信息(BeanDefinition),現在我們開始分析依賴注入的原理。首先需要說明的是依賴注入在用戶第一次向IoC容器索要Bean時觸發,當然也有例外,我們可以在BeanDefinition中中通過控制lazy-init屬性來讓容器完成對Bean的預實例化。這個預實例化實際上也是一個依賴注入的過程,但它是在初始化過程中完成的。

 

二、源碼分析

2.1、getBean()的過程

  接着前面看refresh()方法,這已經是refresh()方法的第三篇博文了,別迷糊我們還沒走出refresh()方法。

 1 // AbstractApplicationContext類
 2 @Override
 3 public void refresh() throws BeansException, IllegalStateException {
 4     synchronized (this.startupShutdownMonitor) {
 5         ...
 6         try {
 7             ...
 8             // Instantiate all remaining (non-lazy-init) singletons.
 9             finishBeanFactoryInitialization(beanFactory);
10             ...
11         }
12         ...
13     }
14 }
15 // AbstractApplicationContext類
16 protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
17     ...
18     // Instantiate all remaining (non-lazy-init) singletons.
19     // 實例化所有剩餘的(non-lazy-init)單例。
20     beanFactory.preInstantiateSingletons();
21 }
22 // DefaultListableBeanFactory類
23 @Override
24 public void preInstantiateSingletons() throws BeansException {
25     ...
26     // Iterate over a copy to allow for init methods which in turn register new bean definitions.
27     // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
28     List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
29     // Trigger initialization of all non-lazy singleton beans...
30     for (String beanName : beanNames) {
31         RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
32         if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
33             if (isFactoryBean(beanName)) {
34                 Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
35                 if (bean instanceof FactoryBean) {
36                     final FactoryBean<?> factory = (FactoryBean<?>) bean;
37                     boolean isEagerInit;
38                     if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
39                         isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
40                                         ((SmartFactoryBean<?>) factory)::isEagerInit,
41                                 getAccessControlContext());
42                     } else {
43                         isEagerInit = (factory instanceof SmartFactoryBean &&
44                                 ((SmartFactoryBean<?>) factory).isEagerInit());
45                     }
46                     if (isEagerInit) {
47                         getBean(beanName);
48                     }
49                 }
50             } else {
51                 // 這裏就是觸發依賴注入的地方
52                 getBean(beanName);
53             }
54         }
55     }
56     ...
57 }

   跟蹤其調用棧,看到上面第52行的getBean(beanName);方法,我們再梳理一下getBean()方法,前面總結過該方法在IoC容器的頂層接口BeanFactory中定義,然後在IoC容器的具體產品DefaultListableBeanFactory類的基類AbstractBeanFactory實現了getBean()方法。接着看代碼。

 1 // AbstractBeanFactory類
 2 @Override
 3 public Object getBean(String name) throws BeansException {
 4     return doGetBean(name, null, null, false);
 5 }
 6 @Override
 7 public <T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException {
 8     return doGetBean(name, requiredType, null, false);
 9 }
10 @Override
11 public Object getBean(String name, Object... args) throws BeansException {
12     return doGetBean(name, null, args, false);
13 }
14 public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
15         throws BeansException {
16     return doGetBean(name, requiredType, args, false);
17 }

  從上面代碼可知大致可分為兩種獲取Bean的參數,一種是按名獲取,一種是按類獲取。但是最終都進入到了doGetBean()方法。

  1 // AbstractBeanFactory類
  2 protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
  3         @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
  4 
  5     // bean獲取過程:先獲取bean名字
  6     // 會把帶有&前綴的去掉,或者去aliasMap中找這個是不是別名,最終確定bean的id是什麼
  7     final String beanName = transformedBeanName(name);
  8     Object bean;
  9 
 10     // 1.檢查緩存中或者實例工廠中是否有對應的實例
 11     // 因為在創建單例bean的時候會存在依賴注入的情況,而在創建依賴的時候為了避免循環依賴
 12     // Spring在創建bean的時候不會等bean創建完成就會將bean的ObjectFactory提早曝光
 13     // 也就是將ObjectFactory加入到緩存中,一旦下一個要創建的bean需要依賴上個bean則直接使用ObjectFactory
 14     // 2.spring 默認是單例的,如果能獲取到直接返回,提高效率。
 15     // Eagerly check singleton cache for manually registered singletons.
 16     Object sharedInstance = getSingleton(beanName);
 17     if (sharedInstance != null && args == null) {
 18         if (logger.isDebugEnabled()) {
 19             if (isSingletonCurrentlyInCreation(beanName)) {
 20                 logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
 21                         "' that is not fully initialized yet - a consequence of a circular reference");
 22             }
 23             else {
 24                 logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
 25             }
 26         }
 27         // 用於檢測bean的正確性,同時如果獲取的是FactoryBean的話還需要調用getObject()方法獲取最終的那個bean實例
 28         bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
 29     }
 30 
 31     else {
 32         // Fail if we're already creating this bean instance:
 33         // We're assumably within a circular reference.
 34         if (isPrototypeCurrentlyInCreation(beanName)) {
 35             throw new BeanCurrentlyInCreationException(beanName);
 36         }
 37 
 38         // Check if bean definition exists in this factory.
 39         //這裏對IoC容器中的BeanDefinition是否存在進行檢查,檢查是否能在當前的BeanFactory中取得需要的Bean。
 40         // 如果當前的工廠中取不到,則到雙親BeanFactory中去取。如果當前的雙親工廠取不到,那就順着雙親BeanFactory
 41         // 鏈一直向上查找。
 42         BeanFactory parentBeanFactory = getParentBeanFactory();
 43         if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
 44             // Not found -> check parent.
 45             String nameToLookup = originalBeanName(name);
 46             if (parentBeanFactory instanceof AbstractBeanFactory) {
 47                 // 遞歸調用父bean的doGetBean查找
 48                 return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
 49                         nameToLookup, requiredType, args, typeCheckOnly);
 50             }
 51             else if (args != null) {
 52                 // Delegation to parent with explicit args.
 53                 return (T) parentBeanFactory.getBean(nameToLookup, args);
 54             }
 55             else {
 56                 // No args -> delegate to standard getBean method.
 57                 return parentBeanFactory.getBean(nameToLookup, requiredType);
 58             }
 59         }
 60 
 61         if (!typeCheckOnly) {
 62             markBeanAsCreated(beanName);
 63         }
 64 
 65         try {
 66             //這裏根據Bean的名字取得BeanDefinition
 67             final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
 68             checkMergedBeanDefinition(mbd, beanName, args);
 69 
 70             // Guarantee initialization of beans that the current bean depends on.
 71             //獲取當前Bean的所有依賴Bean,這裡會觸發getBean的遞歸調用。知道取到一個沒有任何依賴的Bean為止。
 72             String[] dependsOn = mbd.getDependsOn();
 73             if (dependsOn != null) {
 74                 for (String dep : dependsOn) {
 75                     if (isDependent(beanName, dep)) {
 76                         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 77                                 "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
 78                     }
 79                     registerDependentBean(dep, beanName);
 80                     try {
 81                         getBean(dep);
 82                     }
 83                     catch (NoSuchBeanDefinitionException ex) {
 84                         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 85                                 "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
 86                     }
 87                 }
 88             }
 89 
 90             // 這裏通過createBean方法創建singleton Bean的實例 這裏還有一個回調函數
 91             // Create bean instance.
 92             if (mbd.isSingleton()) {
 93                 sharedInstance = getSingleton(beanName, () -> {
 94                     try {
 95                         // 最後在getSingleton中又會調用這個方法
 96                         // TODO createBean的入口
 97                         return createBean(beanName, mbd, args);
 98                     }
 99                     catch (BeansException ex) {
100                         // Explicitly remove instance from singleton cache: It might have been put there
101                         // eagerly by the creation process, to allow for circular reference resolution.
102                         // Also remove any beans that received a temporary reference to the bean.
103                         destroySingleton(beanName);
104                         throw ex;
105                     }
106                 });
107                 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
108             }
109             // 這裡是創建prototype bean的地方
110             else if (mbd.isPrototype()) {
111                 // It's a prototype -> create a new instance.
112                 Object prototypeInstance = null;
113                 try {
114                     beforePrototypeCreation(beanName);
115                     prototypeInstance = createBean(beanName, mbd, args);
116                 }
117                 finally {
118                     afterPrototypeCreation(beanName);
119                 }
120                 bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
121             }
122 
123             else {
124                 String scopeName = mbd.getScope();
125                 final Scope scope = this.scopes.get(scopeName);
126                 if (scope == null) {
127                     throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
128                 }
129                 try {
130                     Object scopedInstance = scope.get(beanName, () -> {
131                         beforePrototypeCreation(beanName);
132                         try {
133                             return createBean(beanName, mbd, args);
134                         }
135                         finally {
136                             afterPrototypeCreation(beanName);
137                         }
138                     });
139                     bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
140                 }
141                 catch (IllegalStateException ex) {
142                     throw new BeanCreationException(beanName,
143                             "Scope '" + scopeName + "' is not active for the current thread; consider " +
144                             "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
145                             ex);
146                 }
147             }
148         }
149         catch (BeansException ex) {
150             cleanupAfterBeanCreationFailure(beanName);
151             throw ex;
152         }
153     }
154 
155     // Check if required type matches the type of the actual bean instance.
156     //這裏對創建的Bean進行類型檢查,如果沒有問題,就返回這個新創建的Bean,這個Bean已經是包含了依賴關係的Bean
157     if (requiredType != null && !requiredType.isInstance(bean)) {
158         try {
159             T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
160             if (convertedBean == null) {
161                 throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
162             }
163             return convertedBean;
164         }
165         catch (TypeMismatchException ex) {
166             if (logger.isDebugEnabled()) {
167                 logger.debug("Failed to convert bean '" + name + "' to required type '" +
168                         ClassUtils.getQualifiedName(requiredType) + "'", ex);
169             }
170             throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
171         }
172     }
173     return (T) bean;
174 }

  這個就是依賴注入的入口了,依賴注入是在容器的BeanDefinition數據已經建立好的前提下進行的。“程序=數據+算法”,很經典的一句話,前面我們詳細介紹了BeanDefinition的註冊過程,BeanDefinition就是數據。如上面代碼所示,doGetBean()方法不涉及複雜的算法,但是這個過程也不是很簡單,因為我們都知道,對於IoC容器的使用,Spring提供了很多的配置參數,每一個配置參數實際上就代表了一個IoC容器的實現特徵,這些特徵很多都需要在依賴注入的過程或者對Bean進行生命周期管理的過程中完成。雖然我們可以簡單的將IoC容器描述成一個ConcurrentHashMap,ConcurrentHashMap只是它的數據結構而不是IoC容器的全部。

TIPS:
  1,我在工程中簡單寫了一個controller和一個service,我們在後面debug看看依賴注入的過程是怎麼樣的,
也不知道我能不能說清楚。希望大家看到這一塊多debug一下,debug技巧如下圖所示,寫了debug的條件,
因為這邊到處都是遞歸和回調函數,再加上有很多Spring的Bean,但是我們只關心自己的Bean,
所以就寫了這樣的過濾條件:beanName.equals("webController")||beanName.equals("webService")
@RestController
public class WebController {
    @Autowired
    private WebService webService;

    @RequestMapping("/web")
    public String web(){
        return webService.hello();
    }
}

 

  下面我們通過代碼看看獲取bean的過程。

  OK,看代碼,Object sharedInstance = getSingleton(beanName);如註釋所說,首先回去找在容器中是不是已經存在該單例。具體在哪找我們在前面的文章中已經說得很清楚了。看一下getSingleton()方法

 1 // DefaultSingletonBeanRegistry類
 2 protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 3     // 由於scope是singleton,所以先從緩存中取單例對象的實例,如果取到直接返回,沒有取到加載bean
 4     Object singletonObject = this.singletonObjects.get(beanName);
 5     // 當想要獲取的bean沒有被加載,並且也沒有正在被創建的時候,主動去加載bean
 6     if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
 7         // 鎖住單例緩存區加載bean
 8         synchronized (this.singletonObjects) {
 9             // singletonObjects ,earlySingletonObjects ,singletonFactories是一個單例實例的三種存在狀態
10             // 再去earlySingletonObjects中去找
11             singletonObject = this.earlySingletonObjects.get(beanName);
12             if (singletonObject == null && allowEarlyReference) {
13                 // 去singletonFactories中去找對象的實例
14                 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
15                 if (singletonFactory != null) {
16                     singletonObject = singletonFactory.getObject();
17                     this.earlySingletonObjects.put(beanName, singletonObject);
18                     this.singletonFactories.remove(beanName);
19                 }
20             }
21         }
22     }
23     return singletonObject;
24 }

 

   在DefaultSingletonBeanRegistry類中的singletonObjects屬性就是存singleton bean的地方。

  如果getSingleton()為 null繼續往下看,會在當前的BeanFactory中獲取BeanDefinition,也就是這行方法代碼:final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);在這行代碼拿到BeanDefinition后,首先判斷是不是singleton Bean,如果是的話,開始執行創建Bean,正是return createBean(beanName, mbd, args);這行代碼。如果是原型(Prototype)Bean我們就不分析了。原型bean每次執行getBean()都會創建一個實例。接下來我們看createBean()方法。

2.2、createBean()的過程

  首先看一下create bean的過程

1,Bean實例的創建
2,為Bean實例設置屬性(屬性注入,其實就是依賴注入真正發生的地方)
3,調用Bean的初始化方法

  前面說了getBean()是依賴注入的起點,之後會調用createBean(),下面通過createBean()代碼來了解這個過程。在這個過程中,Bean對象會根據BeanDefinition定義的要求生成。

 1 // AbstractAutowireCapableBeanFactory類
 2 @Override
 3 protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
 4         throws BeanCreationException {
 5     ...
 6     try {
 7         // 驗證以及準備override的方法
 8         mbdToUse.prepareMethodOverrides();
 9     }
10     catch (BeanDefinitionValidationException ex) {
11         throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),
12                 beanName, "Validation of method overrides failed", ex);
13     }
14     try {
15         // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
16         // createBean之前調用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法
17         // 默認不做任何處理所以會返回null
18         // 但是如果我們重寫了這兩個方法,那麼bean的創建過程就結束了,這裏就為以後的annotation自動注入提供了鈎子
19         Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
20         if (bean != null) {
21             return bean;
22         }
23     }catch (Throwable ex) {
24         throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
25                 "BeanPostProcessor before instantiation of bean failed", ex);
26     }
27     try {
28         // 實際執行createBean的是doCreateBean()方法
29         Object beanInstance = doCreateBean(beanName, mbdToUse, args);
30         if (logger.isDebugEnabled()) {
31             logger.debug("Finished creating instance of bean '" + beanName + "'");
32         }
33         return beanInstance;
34     }
35     catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
36         // A previously detected exception with proper bean creation context already,
37         // or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry.
38         throw ex;
39     }
40     catch (Throwable ex) {
41         throw new BeanCreationException(
42                 mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
43     }
44 }

   接着往下看doCreateBean()方法。

  1 // AbstractAutowireCapableBeanFactory類
  2 protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
  3         throws BeanCreationException {
  4     // BeanWrapper是用來持有創建出來的Bean對象
  5     // Instantiate the bean.
  6     BeanWrapper instanceWrapper = null;
  7     // 如果是單例,先把緩存中的同名Bean清除
  8     if (mbd.isSingleton()) {
  9         instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
 10     }
 11     // 這裡是創建Bean的地方,由createBeanInstance完成。
 12     // TODO 完成Bean初始化過程的第一步:創建實例
 13     if (instanceWrapper == null) {
 14         instanceWrapper = createBeanInstance(beanName, mbd, args);
 15     }
 16     final Object bean = instanceWrapper.getWrappedInstance();
 17     Class<?> beanType = instanceWrapper.getWrappedClass();
 18     if (beanType != NullBean.class) {
 19         mbd.resolvedTargetType = beanType;
 20     }
 21 
 22     // Allow post-processors to modify the merged bean definition.
 23     synchronized (mbd.postProcessingLock) {
 24         if (!mbd.postProcessed) {
 25             try {
 26                 applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
 27             }
 28             catch (Throwable ex) {
 29                 throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 30                         "Post-processing of merged bean definition failed", ex);
 31             }
 32             mbd.postProcessed = true;
 33         }
 34     }
 35 
 36     // Eagerly cache singletons to be able to resolve circular references
 37     // even when triggered by lifecycle interfaces like BeanFactoryAware.
 38     // 是否自動解決循環引用
 39     // 當bean條件為: 單例&&允許循環引用&&正在創建中這樣的話提早暴露一個ObjectFactory
 40     boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
 41             isSingletonCurrentlyInCreation(beanName));
 42     if (earlySingletonExposure) {
 43         if (logger.isDebugEnabled()) {
 44             logger.debug("Eagerly caching bean '" + beanName +
 45                     "' to allow for resolving potential circular references");
 46         }
 47         // 把ObjectFactory放進singletonFactories中
 48         // 這裡在其他bean在創建的時候會先去singletonFactories中查找有沒有beanName到ObjectFactory的映射
 49         // 如果有ObjectFactory就調用它的getObject方法獲取實例
 50         // 但是在這裏就可以對一個bean進行保證,代理等等AOP就可以在getEarlyBeanReference這裏實現
 51         addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
 52     }
 53 
 54     // Initialize the bean instance.
 55     Object exposedObject = bean;
 56     try {
 57         // TODO 完成Bean初始化過程的第二步:為Bean的實例設置屬性
 58         // Bean依賴注入發生的地方
 59         // 對bean進行屬性填充,如果存在依賴於其他的bean的屬性,則會遞歸的調用初始化依賴的bean
 60         populateBean(beanName, mbd, instanceWrapper);
 61         // TODO 完成Bean初始化過程的第三步:調用Bean的初始化方法(init-method)
 62         // 調用初始化方法,比如init-method方法指定的方法
 63         exposedObject = initializeBean(beanName, exposedObject, mbd);
 64     }
 65     catch (Throwable ex) {
 66         if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
 67             throw (BeanCreationException) ex;
 68         }
 69         else {
 70             throw new BeanCreationException(
 71                     mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
 72         }
 73     }
 74 
 75     if (earlySingletonExposure) {
 76         Object earlySingletonReference = getSingleton(beanName, false);
 77         if (earlySingletonReference != null) {
 78             if (exposedObject == bean) {
 79                 exposedObject = earlySingletonReference;
 80             }
 81             else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
 82                 String[] dependentBeans = getDependentBeans(beanName);
 83                 Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
 84                 for (String dependentBean : dependentBeans) {
 85                     if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
 86                         actualDependentBeans.add(dependentBean);
 87                     }
 88                 }
 89                 if (!actualDependentBeans.isEmpty()) {
 90                     throw new BeanCurrentlyInCreationException(beanName,
 91                             "Bean with name '" + beanName + "' has been injected into other beans [" +
 92                             StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
 93                             "] in its raw version as part of a circular reference, but has eventually been " +
 94                             "wrapped. This means that said other beans do not use the final version of the " +
 95                             "bean. This is often the result of over-eager type matching - consider using " +
 96                             "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
 97                 }
 98             }
 99         }
100     }
101 
102     // Register bean as disposable.
103     try {
104         // 註冊銷毀方法,比如:可以在配置bean的時候指定destory-method方法
105         registerDisposableBeanIfNecessary(beanName, bean, mbd);
106     }
107     catch (BeanDefinitionValidationException ex) {
108         throw new BeanCreationException(
109                 mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
110     }
111 
112     return exposedObject;
113 }

  結合上面的代碼,我們再來看一下創建Bean的三個步驟,是不是有點豁然開朗的感覺。別著急繼續往下看。

1,Bean實例的創建,instanceWrapper = createBeanInstance(beanName, mbd, args);
2,為Bean實例設置屬性,populateBean(beanName, mbd, instanceWrapper);
3,調用Bean的初始化方法,exposedObject = initializeBean(beanName, exposedObject, mbd);

 

2.2.1、createBeanInstance():Bean實例的創建

  看代碼

 1 // AbstractAutowireCapableBeanFactory類
 2 protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
 3     // Make sure bean class is actually resolved at this point.
 4     // 確認需要創建的Bean的實例的類可以實例化
 5     Class<?> beanClass = resolveBeanClass(mbd, beanName);
 6 
 7     if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
 8         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 9                 "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
10     }
11 
12     Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
13     if (instanceSupplier != null) {
14         return obtainFromSupplier(instanceSupplier, beanName);
15     }
16 
17     // 當有工廠方法的時候使用工廠方法初始化Bean,就是配置的時候指定FactoryMethod屬性,類似註解中的@Bean把方法的返回值作為Bean
18     if (mbd.getFactoryMethodName() != null)  {
19         return instantiateUsingFactoryMethod(beanName, mbd, args);
20     }
21 
22     // Shortcut when re-creating the same bean...
23     boolean resolved = false;
24     boolean autowireNecessary = false;
25     if (args == null) {
26         synchronized (mbd.constructorArgumentLock) {
27             if (mbd.resolvedConstructorOrFactoryMethod != null) {
28                 resolved = true;
29                 autowireNecessary = mbd.constructorArgumentsResolved;
30             }
31         }
32     }
33     if (resolved) {
34         if (autowireNecessary) {
35             // 如果有有參數的構造函數,構造函數自動注入
36             // 這裏spring會花費大量的精力去進行參數的匹配
37             return autowireConstructor(beanName, mbd, null, null);
38         }
39         else {
40             // 如果沒有有參構造函數,使用默認構造函數構造
41             return instantiateBean(beanName, mbd);
42         }
43     }
44 
45     // Need to determine the constructor...
46     // 使用構造函數進行實例化
47     Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
48     if (ctors != null ||
49             mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
50             mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
51         return autowireConstructor(beanName, mbd, ctors, args);
52     }
53 
54     // No special handling: simply use no-arg constructor.
55     // 使用默認的構造函數對Bean進行實例化
56     return instantiateBean(beanName, mbd);
57 }

 

  我們可以看到在instantiateBean()方法中生成了Bean所包含的Java對象,這個對象的生成有很多種不同的方式,可以通過工廠方法生成,也可以通過容器的autowire特性生成,這些生成方式都是由BeanDefinition決定的。對於上面我們的WebController和WebService兩個類是通過最後一行,使用默認的構造函數進行Bean的實例化。

  接着看instantiateBean()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
 3     // 使用默認的實例化策略對Bean進行實例化,默認的實例化策略是CglibSubclassingInstantiationStrategy,
 4     // 也就是常說的CGLIB來對Bean進行實例化。PS:面試官常問的字節碼增強
 5     try {
 6         Object beanInstance;
 7         final BeanFactory parent = this;
 8         if (System.getSecurityManager() != null) {
 9             beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () ->
10                     getInstantiationStrategy().instantiate(mbd, beanName, parent),
11                     getAccessControlContext());
12         }
13         else {
14             // getInstantiationStrategy()會返回CglibSubclassingInstantiationStrategy類的實例
15             beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
16         }
17         BeanWrapper bw = new BeanWrapperImpl(beanInstance);
18         initBeanWrapper(bw);
19         return bw;
20     }
21     catch (Throwable ex) {
22         throw new BeanCreationException(
23                 mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
24     }
25 }

  這裏使用CGLIB進行Bean的實例化。CGLIB是一個常用的字節碼生成器的類庫,其提供了一系列的API來提供生成和轉換Java字節碼的功能。在Spring AOP中同樣也是使用的CGLIB對Java的字節碼進行增強。在IoC容器中,使用SimpleInstantiationStrategy類。這個類是Spring用來生成Bean對象的默認類,它提供了兩種實例化Java對象的方法,一種是通過BeanUtils,它使用的是JVM的反射功能,一種是通過CGLIB來生成。

  getInstantiationStrategy()方法獲取到CglibSubclassingInstantiationStrategy實例,instantiate()是CglibSubclassingInstantiationStrategy類的父類SimpleInstantiationStrategy實現的。

  繼續看代碼

 1 // SimpleInstantiationStrategy類
 2 @Override
 3 public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
 4     // Don't override the class with CGLIB if no overrides.
 5     // 如果BeanFactory重寫了Bean內的方法,則使用CGLIB,否則使用BeanUtils
 6     if (!bd.hasMethodOverrides()) {
 7         // 如果bean沒有需要動態替換的方法就直接反射進行創建實例
 8         Constructor<?> constructorToUse;
 9         synchronized (bd.constructorArgumentLock) {
10             constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
11             if (constructorToUse == null) {
12                 final Class<?> clazz = bd.getBeanClass();
13                 if (clazz.isInterface()) {
14                     throw new BeanInstantiationException(clazz, "Specified class is an interface");
15                 }
16                 try {
17                     if (System.getSecurityManager() != null) {
18                         constructorToUse = AccessController.doPrivileged(
19                                 (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
20                     } else {
21                         constructorToUse = clazz.getDeclaredConstructor();
22                     }
23                     bd.resolvedConstructorOrFactoryMethod = constructorToUse;
24                 } catch (Throwable ex) {
25                     throw new BeanInstantiationException(clazz, "No default constructor found", ex);
26                 }
27             }
28         }
29         // 通過BeanUtils進行實例化,這個BeanUtils的實例化通過Constructor類實例化Bean
30         // 在BeanUtils中可以看到具體的調用ctor.newInstances(args)
31         return BeanUtils.instantiateClass(constructorToUse);
32     } else {
33         // Must generate CGLIB subclass.
34         // TODO 使用CGLIB實例化對象
35         return instantiateWithMethodInjection(bd, beanName, owner);
36     }
37 }

  在SpringBoot中我們一般採用@Autowire的方式進行依賴注入,很少採用像SpringMVC那種在xml中使用<lookup-method>或者<replaced-method>等標籤的方式對注入的屬性進行override,所以在上面的代碼中if(!bd.hasMethodOverrides())中的判斷為true,會採用BeanUtils的實例化方式。

 

2.2.2、populateBean();屬性設置(依賴注入)

  看代碼

 1 protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
 2     if (bw == null) {
 3         if (mbd.hasPropertyValues()) {
 4             throw new BeanCreationException(
 5                     mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
 6         }
 7         else {
 8             // Skip property population phase for null instance.
 9             return;
10         }
11     }
12     // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
13     // state of the bean before properties are set. This can be used, for example,
14     // to support styles of field injection.
15     boolean continueWithPropertyPopulation = true;
16     // 調用InstantiationAwareBeanPostProcessor  Bean的後置處理器,在Bean注入屬性前改變BeanDefinition的信息
17     if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
18         for (BeanPostProcessor bp : getBeanPostProcessors()) {
19             if (bp instanceof InstantiationAwareBeanPostProcessor) {
20                 InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
21                 if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
22                     continueWithPropertyPopulation = false;
23                     break;
24                 }
25             }
26         }
27     }
28     if (!continueWithPropertyPopulation) {
29         return;
30     }
31     // 這裏取得在BeanDefinition中設置的property值,這些property來自對BeanDefinition的解析
32     // 用於在配置文件中通過<property>配置的屬性並且显示在配置文件中配置了autowireMode屬性
33     PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
34     if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||
35             mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
36         MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
37 
38         // Add property values based on autowire by name if applicable.
39         // 這裏對autowire注入的處理,autowire by name
40         if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
41             autowireByName(beanName, mbd, bw, newPvs);
42         }
43 
44         // Add property values based on autowire by type if applicable.
45         // 這裏對autowire注入的處理, autowire by type
46         // private List<Test> tests;
47         if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
48             autowireByType(beanName, mbd, bw, newPvs);
49         }
50 
51         pvs = newPvs;
52     }
53     boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
54     boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);
55     if (hasInstAwareBpps || needsDepCheck) {
56         if (pvs == null) {
57             pvs = mbd.getPropertyValues();
58         }
59         PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
60         if (hasInstAwareBpps) {
61             for (BeanPostProcessor bp : getBeanPostProcessors()) {
62                 if (bp instanceof InstantiationAwareBeanPostProcessor) {
63                     InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
64                     // TODO @Autowire @Resource @Value @Inject 等註解的依賴注入過程
65                     pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
66                     if (pvs == null) {
67                         return;
68                     }
69                 }
70             }
71         }
72         if (needsDepCheck) {
73             checkDependencies(beanName, mbd, filteredPds, pvs);
74         }
75     }
76     if (pvs != null) {
77         // 注入配置文件中<property>配置的屬性
78         applyPropertyValues(beanName, mbd, bw, pvs);
79     }
80 }

  上面方法中的31-52行以及78行的applyPropertyValues()方法基本都是用於SpringMVC中採用xml配置Bean的方法。所以我們不做介紹了。看註釋知道幹嘛的就行了。我們主要看的是pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);這行代碼,這行代碼是真正執行採用@Autowire @Resource @Value @Inject 等註解的依賴注入過程。

  接着往下看

 1 // AutowiredAnnotationBeanPostProcessor類
 2 @Override
 3 public PropertyValues postProcessPropertyValues(
 4         PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
 5     // 遍歷,獲取@Autowire,@Resource,@Value,@Inject等具備註入功能的註解
 6     InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
 7     try {
 8         // 屬性注入
 9         metadata.inject(bean, beanName, pvs);
10     } catch (BeanCreationException ex) {
11         throw ex;
12     } catch (Throwable ex) {
13         throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
14     }
15     return pvs;
16 }

  AutowiredAnnotationBeanPostProcessor類實現了postProcessPropertyValues()方法。findAutowiringMetadata(beanName, bean.getClass(), pvs);方法會尋找在當前類中的被@Autowire,@Resource,@Value,@Inject等具備註入功能的註解的屬性。

  debug看一下結果,如下圖所示,成功得到了@Autowire註解的屬性webService。

  metadata.inject(bean, beanName, pvs);方法開始執行注入的邏輯。

 1 // AutowiredAnnotationBeanPostProcessor類
 2 @Override
 3 protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
 4     // 需要注入的字段
 5     Field field = (Field) this.member;
 6     // 需要注入的屬性值
 7     Object value;
 8     if (this.cached) {
 9         value = resolvedCachedArgument(beanName, this.cachedFieldValue);
10     } else {
11         // @Autowired(required = false),當在該註解中設置為false的時候,如果有直接注入,沒有跳過,不會報錯。
12         DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
13         desc.setContainingClass(bean.getClass());
14         Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
15         Assert.state(beanFactory != null, "No BeanFactory available");
16         TypeConverter typeConverter = beanFactory.getTypeConverter();
17         try {
18             // 通過BeanFactory 解決依賴關係
19             // 比如在webController中注入了webService,這個會去BeanFactory中去獲取webService,也就是getBean()的邏輯。
20             // 如果存在直接返回,不存在再執行createBean()邏輯。
21             // 如果在webService中依然依賴,依然會去遞歸。
22             // 這裡是一個複雜的遞歸邏輯。
23             value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
24         } catch (BeansException ex) {
25             throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
26         }
27         synchronized (this) {
28             if (!this.cached) {
29                 if (value != null || this.required) {
30                     this.cachedFieldValue = desc;
31                     registerDependentBeans(beanName, autowiredBeanNames);
32                     if (autowiredBeanNames.size() == 1) {
33                         String autowiredBeanName = autowiredBeanNames.iterator().next();
34                         if (beanFactory.containsBean(autowiredBeanName) &&
35                                 beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
36                             this.cachedFieldValue = new ShortcutDependencyDescriptor(
37                                     desc, autowiredBeanName, field.getType());
38                         }
39                     }
40                 } else {
41                     this.cachedFieldValue = null;
42                 }
43                 this.cached = true;
44             }
45         }
46     }
47     if (value != null) {
48         ReflectionUtils.makeAccessible(field);
49         field.set(bean, value);
50     }
51 }

  debug看一下field。如下圖所示正是我們的webService

  看這行代碼:value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);注意beanFactory依舊是我們熟悉的IoC容器的具體產品,也就是實現類DefaultListableBeanFactory。見到就說一遍,方便大家記住它,很重要。

  在resolveDependency()方法中經過一頓操作,最終又會來到上面的getBean()方法。以上就是依賴注入的整個過程。注意看代碼中的註釋哦。

2.2.3、initializeBean():調用Bean的初始化方法

  設置Bean的初始化方法有兩種方法,一種是在xml或者@Bean指定init-method方法。另一種是讓bean實現InitializingBean接口重寫afterPropertiesSet()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
 3     if (System.getSecurityManager() != null) {
 4         AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
 5             invokeAwareMethods(beanName, bean);
 6             return null;
 7         }, getAccessControlContext());
 8     }
 9     else {
10         //在調用Bean的初始化方法之前,調用一系列的aware接口實現,把相關的BeanName,BeanClassLoader,以及BeanFactory注入到Bean中去。
11         invokeAwareMethods(beanName, bean);
12     }
13 
14     Object wrappedBean = bean;
15     if (mbd == null || !mbd.isSynthetic()) {
16         // 這些都是鈎子方法,在反覆的調用,給Spring帶來了極大的可拓展性
17         // 初始化之前調用BeanPostProcessor
18         wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
19     }
20 
21     try {
22         // 調用指定的init-method方法
23         invokeInitMethods(beanName, wrappedBean, mbd);
24     }
25     catch (Throwable ex) {
26         throw new BeanCreationException(
27                 (mbd != null ? mbd.getResourceDescription() : null),
28                 beanName, "Invocation of init method failed", ex);
29     }
30     if (mbd == null || !mbd.isSynthetic()) {
31         // 這些都是鈎子方法,在反覆的調用,給Spring帶來了極大的可拓展性
32         // 初始化之後調用BeanPostProcessor
33         wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
34     }
35 
36     return wrappedBean;
37 }

  在調用Bean的初始化方法之前,調用一系列的aware接口實現,把相關的BeanName,BeanClassLoader,以及BeanFactory注入到Bean中去。接着會執行invokeInitMethods()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
 3         throws Throwable {
 4     // 除了使用init-method指定的初始化方法,還可以讓bean實現InitializingBean接口重寫afterPropertiesSet()方法
 5     boolean isInitializingBean = (bean instanceof InitializingBean);
 6     if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
 7         if (logger.isDebugEnabled()) {
 8             logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
 9         }
10         if (System.getSecurityManager() != null) {
11             try {
12                 AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
13                     ((InitializingBean) bean).afterPropertiesSet();
14                     return null;
15                 }, getAccessControlContext());
16             }
17             catch (PrivilegedActionException pae) {
18                 throw pae.getException();
19             }
20         }
21         else {
22             // 執行afterPropertiesSet()方法進行初始化
23             ((InitializingBean) bean).afterPropertiesSet();
24         }
25     }
26 
27     // 先執行afterPropertiesSet()方法,再進行init-method
28     if (mbd != null && bean.getClass() != NullBean.class) {
29         String initMethodName = mbd.getInitMethodName();
30         if (StringUtils.hasLength(initMethodName) &&
31                 !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
32                 !mbd.isExternallyManagedInitMethod(initMethodName)) {
33             invokeCustomInitMethod(beanName, bean, mbd);
34         }
35     }
36 }

  可見該方法中首先判斷Bean是否配置了init-method方法,如果有,那麼通過invokeCustomInitMethod()方法來直接調用。其中在invokeCustomInitMethod()方法中是通過JDK的反射機製得到method對象,然後調用的init-method。最終完成Bean的初始化。

 

三、總結

  SpringBoot啟動流程相關的博文到這裏就結束了,在前面的文章中,我們詳細介紹了IoC容器的設計與實現,並結合SpringBoot的啟動流程介紹了IoC容器的初始化過程,及IoC容器的依賴注入,及大家都很關心的SpringBoot是如何實現自動裝配的。關於SpringBoot的源碼分析基本就到這裏了,後面有計劃寫寫AOP的實現,以及很重要的Spring事務實現。

  小半個月時間,熬了很多個凌晨,終於寫完了SpringBoot啟動流程系列博文。有辛苦更有收穫,我是從今年才開始寫博客的,以前每當想寫的時候,總想什麼時候能力足夠了,憋個大招再寫。回過頭來看之前的想法是十分錯誤的,寫博文不只是為了分享,最重要的是在寫博客的過程中能夠系統的梳理一下某一個技術棧的知識。好記性不如爛筆頭,看別人的博客很難get到自己想要的點,所以也是讓自己持續進步的一種方式。加油,各位。

 

 

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

你知道和你不知道的冒泡排序

這篇文章包含了你一定知道的,和你不一定知道的冒泡排序。

gif看不了可以點擊【原文】查看gif。

1. 什麼是冒泡排序

可能對於大多數的人來說比如我,接觸的第一個算法就是冒泡排序。

我看過的很多的文章都把冒泡排序描述成我們喝的汽水,底部不停的有二氧化碳的氣泡往上冒,還有描述成魚吐泡泡,都特別的形象。

其實結合一杯水來對比很好理解,將我們的數組豎著放進杯子,數組中值小的元素密度相對較小,值大的元素密度相對較大。這樣一來,密度大的元素就會沉入杯底,而密度小的元素會慢慢的浮到杯子的最頂部,稍微專業一點描述如下。

冒泡算法會運行多輪,每一輪會依次比較數組中相鄰的兩個元素的大小,如果左邊的元素大於右邊的元素,則交換兩個元素的位置。最終經過多輪的排序,數組最終成為有序數組。

2. 排序過程展示

我們先不聊空間複雜度和時間複雜度的概念,我們先通過一張動圖來了解一下冒泡排序的過程。

這個圖形象的還原了密度不同的元素上浮和下沉的過程。

3. 算法V1

3.1 代碼實現

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr.length - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        exchange(arr, j, j + 1);
      }
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

3.2 實現分析

各位大佬看了上面的代碼之後先別激動,坐下坐下,日常操作。可能很多的第一個冒泡排序算法就是這麼寫的,比如我,同時還自我感覺良好,覺得算法也不過如此。

我們還是以數組[5, 1, 3, 7, 6, 2, 4]為例,我們通過動圖來看一下過程。

思路很簡單,我們用兩層循環來實現冒泡排序。

  • 第一層,控制冒泡排序總共執行的輪數,例如例子數組的長度是7,那麼總共需要執行6輪。如果長度是n,則需要執行n-1輪
  • 第二層,負責從左到右依次的兩兩比較相鄰元素,並且將大的元素交換到右側

這就是冒泡排序V1的思路。

下錶是通過對一個0-100000的亂序數組的標準樣本,使用V1算法進行排序所總共執行的次數,以及對同一個數組執行100次V1算法的所花的平均時間。

算法執行情況 結果
樣本 [0 – 100000] 的亂序數組
算法 V1 執行的總次數 99990000 次(9999萬次
算法 V1 運行 100 次的平均時間 181 ms

4. 算法V2

4.1 實現分析

仔細看動圖我們可以發現,每一輪的排序,都從數組的最左端再到最右。而每一輪的冒泡,都可以確定一個最大的數,固定在數組的最右邊,也就是密度最大的元素會冒泡到杯子的最上面。

還是拿上面的數組舉例子。下圖是第一輪冒泡之後數組的元素位置。

第二輪排序之後如下。

可以看到,每一輪排序都會確認一個最大元素,放在數組的最後面,當算法進行到後面,我們根本就沒有必要再去比較數組後面已經有序的片段,我們接下來針對這個點來優化一下。

4.2 代碼實現

這是優化之後的代碼。

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        exchange(arr, j, j + 1);
      }
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

優化之後的實現,也就變成了我們動圖中所展示的過程。

每一步之後都會確定一個元素在數組中的位置,所以之後的每次冒泡的需要比較的元素個數就會相應的減1。這樣一來,避免了去比較已經有序的數組,從而減少了大量的時間。

算法執行情況 結果
樣本 [0 – 10000] 的亂序數組
算法 V2 執行的總次數 49995000 次(4999萬次
算法 V2 運行 100 次的平均時間 144 ms
運行時間與 V1 對比 V2 運行時間減少 20.44 %
執行次數與 V1 對比 V2 運行次數減少 50.00 %

可能會有人看到,時間大部分已經會覺得滿足了。從數據上看,執行的次數減少了50%,而運行的時間也減少了20%,在性能上已經是很大的提升了。而且已經減少了7億次的執行次數,已經很NB了。 那是不是到這就已經很完美了呢?

答案是No

4.3 哪裡可以優化

同理,我們還是拿上面長度為7的數組來舉例子,只不過元素的位置有所不同,假設數組的元素如下。

[7, 1, 2, 3, 4, 5, 6]

我們再來一步一步的執行V2算法, 看看會發生什麼。

第一步執行完畢后,數組的情況如下。

繼續推進,當第一輪執行完畢后,數組的元素位置如下。

這個時候,數組已經排序完畢,但是按照目前的V2邏輯,仍然有5輪排序需要繼續,而且程序會完整的執行完5輪的排序,如果是100000輪呢?這樣將會浪費大量的計算資源。

5. 算法V3

5.1 代碼實現

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    boolean flag = true;
    for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        exchange(arr, j, j + 1);
      }
    }
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

5.2 實現分析

我們在V2代碼的基礎上,在第一層循環,也就是控制總冒泡輪數的循環中,加入了一個標誌為flag。用來標示該輪冒泡排序中,數組是否是有序的。每一輪的初始值都是true。

當第二層循環,也就是冒泡排序的元素兩兩比較完成之後,flag的值仍然是true,則說明在這輪比較中沒有任何元素被交換了位置。也就是說,數組此時已經是有序狀態了,沒有必要再執行後續的剩餘輪數的冒泡了。

所以,如果flag的值是true,就直接break了(沒有其他的操作return也沒毛病)。

算法執行情況 結果
樣本 [0 – 10000] 的亂序數組
算法 V3 執行的總次數 49993775
算法 V3 運行 100 次的平均時間 142 ms
運行時間與 V2 對比 V3 運行時間減少 00.00 %
執行次數與 V2 對比 V3 運行次數減少 00.00 %

5.3 數據分析

大家看到數據可能有點懵逼。

你這個優化之後,運行時間執行次數都沒有減少。你這優化的什麼東西?

其實,這就要說到算法的適用性了。V3的優化是針對原始數據中存在一部分或者大量的數據已經是有序的情況,V3的算法對於這樣的樣本數據才最適用。

其實是我們還沒有到優化這種情況的那一步,但是其實仍然有這樣的說法,面對不同的數據結構,幾乎沒有算法是萬能的

而目前的樣本數據仍然是隨機的亂序數組,所以並不能發揮優化之後的算法的威力。所謂對症下藥,同理並不是所有的算法都是萬能的。對於不同的數據我們需要選擇不同的算法。例如我們選擇[9999,1,2,…,9998]這行的數據做樣本來分析,我們來看一下V3算法的表現。

算法執行情況 結果
樣本 [0 – 10000] 的亂序數組
算法 V3 執行的總次數 19995
算法 V3 運行 100 次的平均時間 1 ms
運行時間與 V3 亂序樣例對比 V3 運行時間減少 99.96 %
執行次數與 V3 亂序樣例對比 V3 運行次數減少 99.29 %

可以看到,提升非常明顯。

5.4 適用情況

當冒泡算法運行到後半段的時候,如果此時數組已經有序了,需要提前結束冒泡排序。V3針對這樣的情況就特別有效。

6. 算法V4

嗯,什麼?為什麼不是結束語?那是因為還有一種沒有考慮到啊。

6.1 適用情況總結

我們總結一下前面的算法能夠處理的情況。

  • V1:正常亂序數組
  • V2:正常亂序數組,但對算法的執行次數做了優化
  • V3:大部分元素已經有序的數組,可以提前結束冒泡排序

還有一種情況是冒泡算法的輪數沒有執行完,甚至還沒有開始執行,後半段的數組就已經有序的數組,例如如下的情況。

這種情況,在數組完全有序之前都不會觸發V3中的提前停止算法,因為每一輪都有交換存在,flag的值會一直是true。而下標2之後的所有的數組都是有序的,算法會依次的冒泡完所有的已有序部分,造成資源的浪費。我們怎麼來處理這種情況呢?

6.2 實現分析

我們可以在V3的基礎之上來做。

當第一輪冒泡排序結束后,元素3會被移動到下標2的位置。在此之後沒有再進行過任意一輪的排序,但是如果我們不做處理,程序仍然會繼續的運行下去。

我們在V3的基礎上,加上一個標識endIndex來記錄這一輪最後的發生交換的位置。這樣一來,下一輪的冒泡就只冒到endIndex所記錄的位置即可。因為後面的數組沒有發生任何的交換,所以數組必定有序。

6.3 代碼實現

private void bubbleSort(int[] arr) {
  int endIndex = arr.length - 1;
  for (int i = 0; i < arr.length - 1; i++) {
    boolean flag = true;
    int endAt = 0;
    for (int j = 0; j < endIndex; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        endAt = j;
        exchange(arr, j, j + 1);
      }
    }
    endIndex = endAt;
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

7. 算法V5

這一節仍然不是結束語…

7.1 算法優化

我們來看一下這種情況。

對於這種以上的算法都將不能發揮其應有的作用。每一輪算法都存在元素的交換,同時,直到算法完成以前,數組都不是有序的。但是如果我們能直接從右向左冒泡,只需要一輪就可以完成排序。這就是雞尾酒排序,冒泡排序的另一種優化,其適用情況就是上圖所展示的那種。

7.2 代碼實現

private void bubbleSort(int[] arr) {
  int leftBorder = 0;
  int rightBorder = arr.length - 1;

  int leftEndAt = 0;
  int rightEndAt = 0;

  for (int i = 0; i < arr.length / 2; i++) {
    boolean flag = true;
    for (int j = leftBorder; j < rightBorder; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        exchange(arr, j, j + 1);
        rightEndAt = j;
      }
    }
    rightBorder = rightEndAt;
    if (flag) {
      break;
    }

    flag = true;
    for (int j = rightBorder; j > leftBorder; j--) {
      if (arr[j] < arr[j - 1]) {
        flag = false;
        exchange(arr, j, j - 1);
        leftEndAt = j;
      }
    }
    leftBorder = leftEndAt;
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{2, 3, 4, 5, 6, 7, 1};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

7.3 實現分析

第一層循環同樣用於控制總的循環輪數,由於每次需要從左到右再從右到左,所以總共的輪數是數組的長度 / 2。

內存循環則負責先實現從左到右的冒泡排序,再實現從右到左的冒泡,並且同時結合了V4的優化點。

我們來看一下V5與V4的對比。

算法執行情況 結果
樣本 [2,3,4…10000,1] 的數組
算法 V5 執行的總次數 19995
算法 V5 運行 100 次的平均時間 1 ms
運行時間與 V4 對比 V5 運行時間減少 99.97 %
執行次數與 V4 對比 V5 運行次數減少 99.34 %

8. 總結

以下是對同一個數組,使用每一種算法對其運行100次的平均時間和執行次數做的的對比。

[0 – 10000] 的亂序數組 V1 V2 V3 V4 V5
執行時間(ms) 184 142 143 140 103
執行次數(次) 99990000 49995000 49971129 49943952 16664191
大部分有序的情況 V1 V2 V3 V4 V5
執行時間(ms) 181 141 146 145 107
執行次數(次) 99990000 49995000 49993230 49923591 16675618

而冒泡排序的時間複雜度分為最好的情況和最快的情況。

  • 最好的情況為O($n$). 也就是我們在V5中提到的那種情況,數組2, 3, 4, 5, 6, 7, 1。使用雞尾酒算法,只需要進行一輪冒泡,即可完成對數組的排序。
  • 最壞的情況為O($n^2$).也就是V1,V2,V3和V4所遇到的情況,幾乎大部分數據都是無序的。

往期文章:

  • 聊聊微服務集群當中的自動化工具
  • go源碼解析-Println的故事
  • 用go-module作為包管理器搭建go的web服務器
  • WebAssembly完全入門——了解wasm的前世今身
  • 小強開飯店-從單體應用到微服務

相關:

  • 微信公眾號: SH的全棧筆記(或直接在添加公眾號界面搜索微信號LunhaoHu)

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

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