用象棋的思維趣說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)

【精選推薦文章】

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

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

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

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

redis的五種數據類型及應用場景

前言

redis是用鍵值對的形式來保存數據,鍵類型只能是String,但是值類型可以有String、List、Hash、Set、Sorted Set五種,來滿足不同場景的特定需求。

本博客中的示例不是將控制台作為redis的一個客戶端,而是將redis運用在java里進行測試

需要有java redis的驅動包,可以通過引入maven的依賴即可

        <dependency>
            <groupId>org.rarefiedredis.redis</groupId>
            <artifactId>redis-java</artifactId>
            <version>0.0.17</version>
        </dependency>

 

String

String類型是最基礎的一種key-value存儲形式,value其實不僅僅可以是String,也可以是數值類型。常常用來做計數器這類自增自減的功能,可用在粉絲數、微博數等。

示例

 1         //連接本地的 Redis 服務
 2         Jedis jedis = new Jedis("localhost");
 3         System.out.println("連接成功");
 4         //查看服務是否運行
 5         System.out.println("服務正在運行: "+jedis.ping());
 6         //String實例
 7         jedis.set("hello", String.valueOf(1));
 8         jedis.incr("hello");
 9         jedis.set("hello1","word1");
10         System.out.println(jedis.get("hello"));
11         System.out.println(jedis.mget("hello","hello1"));

常用命令

  • set
  • get
  • mget
  • incr
  • decr

 

List

list就是鏈表,在redis實現為一個雙向鏈表,可以從兩邊插入、刪除數據。應用場景可以有微博的關注列表、粉絲列表、消息列表等。

有一個lrange函數,可以從某個元素開始讀取多少個元素,可用來實現分頁功能。

示例

 1         /*list實例,雙向鏈表結構,適合做消息隊列,
 2         但其實真正應用中一般都會用專門做消息隊列的中間件例如RabbitMQ*/
 3         jedis.lpush("201宿舍","hlf");
 4         jedis.lpush("201宿舍","css");
 5         jedis.lpush("201宿舍","ty");
 6         jedis.lpush("201宿舍","jy");
 7         List<String> name = jedis.lrange("201宿舍",0,3);
 8         for (String person:name
 9              ) {
10             System.out.print(person+" ");
11         }

 

常用命令

  •  lpush
  • rpush
  • lpush
  • lpop
  • lrange

 

Hash

hash就是值類型存儲的是一個鍵值對形式,適合存儲對象類型信息,例如個人信息、商品信息等。

示例

 1         //hash實例,適合存儲對象
 2         HashMap<String,String> map = new HashMap<String, String>();
 3         map.put("name","hlf");
 4         map.put("sex","女");
 5         map.put("age","21");
 6         jedis.hmset("hlf",map);
 7         jedis.hset("hlf","major","software");
 8         Map<String,String> map1 = jedis.hgetAll("hlf");
 9         String age = jedis.hget("hlf","age");
10         System.out.println(map1);
11         System.out.println(age);

 

常用命令

  • hset
  • hmset
  • hget
  • hgetAll

 

Set

set表示存儲的一個元素不重合的集合,因為set集合支持查緝、並集操作,因此適合做共同好友等功能

示例

1         //set實例
2         jedis.sadd("set","hhh");
3         jedis.sadd("set","ff");
4         jedis.sadd("set","hhh");
5         System.out.println(jedis.smembers("set"));
6         jedis.sadd("set1","oo");
7         jedis.sadd("set1","ff");
8         System.out.println("交集:"+jedis.sinter("set","set1"));
9         System.out.println("合集:"+jedis.sunion("set","set1"));

 

常用命令

  • sadd
  • spop
  • smembers
  • sunion
  • sinter

 

Sorted Set

相對於Set,Sorted Set多了一個Score作為權重,使集合裏面的元素可以按照score排序,注意它是Set,所以它裏面的元素也不能重複

示例

        //sorted set實例
        jedis.zadd("set2",4,"redis");
        jedis.zadd("set2",3,"mysql");
        jedis.zadd("set2",2,"kk");
        jedis.zadd("set2",1,"redis");
        System.out.println(jedis.zrangeByScore("set2",0,4));

 

常用命令

  • zadd
  • zpop
  • zrangeByScore

 

【精選推薦文章】

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

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

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

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

高級Java工程師必備 —– 深入分析 Java IO (二)NIO

接着上一篇文章 高級Java工程師必備 —– 深入分析 Java IO (一)BIO,我們來講講NIO

多路復用IO模型

場景描述

一個餐廳同時有100位客人到店,當然到店后第一件要做的事情就是點菜。但是問題來了,餐廳老闆為了節約人力成本目前只有一位大堂服務員拿着唯一的一本菜單等待客人進行服務。

方法A: 無論有多少客人等待點餐,服務員都把僅有的一份菜單遞給其中一位客人,然後站在客人身旁等待這個客人完成點菜過程。在記錄客人點菜內容后,把點菜記錄交給後堂廚師。然後是第二位客人。。。。然後是第三位客人。很明顯,只有腦袋被門夾過的老闆,才會這樣設置服務流程。因為隨後的80位客人,再等待超時后就會離店(還會給差評)。

方法B: 老闆馬上新雇傭99名服務員,同時印製99本新的菜單。每一名服務員手持一本菜單負責一位客人(關鍵不只在於服務員,還在於菜單。因為沒有菜單客人也無法點菜)。在客人點完菜后,記錄點菜內容交給後堂廚師(當然為了更高效,後堂廚師最好也有100名)。這樣每一位客人享受的就是VIP服務咯,當然客人不會走,但是人力成本可是一個大頭哦(虧死你)。

方法C: 就是改進點菜的方式,當客人到店后,自己申請一本菜單。想好自己要點的才后,就呼叫服務員。服務員站在自己身邊後記錄客人的菜單內容。將菜單遞給廚師的過程也要進行改進,並不是每一份菜單記錄好以後,都要交給後堂廚師。服務員可以記錄號多份菜單后,同時交給廚師就行了。那麼這種方式,對於老闆來說人力成本是最低的;對於客人來說,雖然不再享受VIP服務並且要進行一定的等待,但是這些都是可接受的;對於服務員來說,基本上她的時間都沒有浪費,基本上被老闆壓桿了最後一滴油水。

到店情況:併發量。到店情況不理想時,一個服務員一本菜單,當然是足夠了。所以不同的老闆在不同的場合下,將會靈活選擇服務員和菜單的配置。
客人:客戶端請求
點餐內容:客戶端發送的實際數據
服務員:操作系統內核用於IO操作的線程(內核線程)
廚師:應用程序線程(當然廚房就是應用程序進程咯)
餐單傳遞方式:包括了阻塞式和非阻塞式兩種。

  • 方法A:阻塞式/非阻塞式 同步IO
  • 方法B:使用線程進行處理的 阻塞式/非阻塞式 同步IO
  • 方法C:阻塞式/非阻塞式 多路復用IO

多路復用IO技術最適用的是“高併發”場景,所謂高併發是指1毫秒內至少同時有上千個連接請求準備好。其他情況下多路復用IO技術發揮不出來它的優勢。另一方面,使用JAVA NIO進行功能實現,相對於傳統的Socket套接字實現要複雜一些,所以實際應用中,需要根據自己的業務需求進行技術選擇。

NIO

概念

JDK 1.4中的java.nio.*包中引入新的Java I/O庫,其目的是提高速度。實際上,“舊”的I/O包已經使用NIO重新實現過,即使我們不顯式的使用NIO編程,也能從中受益。速度的提高在文件I/O和網絡I/O中都可能會發生,但本文只討論後者。

NIO我們一般認為是New I/O(也是官方的叫法),因為它是相對於老的I/O類庫新增的(其實在JDK 1.4中就已經被引入了,但這個名詞還會繼續用很久,即使它們在現在看來已經是“舊”的了,所以也提示我們在命名時,需要好好考慮),做了很大的改變。但民間跟多人稱之為Non-block I/O,即非阻塞I/O,因為這樣叫,更能體現它的特點。而下文中的NIO,不是指整個新的I/O庫,而是非阻塞I/O。

面向流與面向緩衝

Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。

面向塊的 NIO一次處理一個數據塊,按塊處理數據比按流處理數據要快得多。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區里尚未處理的數據。

阻塞與非阻塞IO

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)

通道

通道 Channel 是對原 I/O 包中的流的模擬,可以通過它讀取和寫入數據。

通道與流的不同之處在於,流只能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,可以用於讀、寫或者同時用於讀寫。

通道包括以下類型:

  • FileChannel:從文件中讀寫數據;
  • DatagramChannel:通過 UDP 讀寫網絡中數據;
  • SocketChannel:通過 TCP 讀寫網絡中數據;
  • ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。

緩衝區

發送給一個通道的所有數據都必須首先放到緩衝區中,同樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先經過緩衝區。

緩衝區實質上是一個數組,但它不僅僅是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。

Buffer有兩種工作模式:寫模式和讀模式。在讀模式下,應用程序只能從Buffer中讀取數據,不能進行寫操作。但是在寫模式下,應用程序是可以進行讀操作的,這就表示可能會出現臟讀的情況。所以一旦您決定要從Buffer中讀取數據,一定要將Buffer的狀態改為讀模式。

注意:ServerSocketChannel通道它只支持對OP_ACCEPT事件的監聽,所以它是不能直接進行網絡數據內容的讀寫的。所以ServerSocketChannel是沒有集成Buffer的。

緩衝區包括以下類型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

 

 

可以用三個值指定緩衝區在任意時刻的狀態:

  • position
  • limit
  • capacity

Position

您可以回想一下,緩衝區實際上就是美化了的數組。在從通道讀取時,您將所讀取的數據放到底層的數組中。 position 變量跟蹤已經寫了多少數據。更準確地說,它指定了下一個字節將放到數組的哪一個元素中。因此,如果您從通道中讀三個字節到緩衝區中,那麼緩衝區的 position 將會設置為3,指向數組中第四個元素。

同樣,在寫入通道時,您是從緩衝區中獲取數據。 position 值跟蹤從緩衝區中獲取了多少數據。更準確地說,它指定下一個字節來自數組的哪一個元素。因此如果從緩衝區寫了5個字節到通道中,那麼緩衝區的 position 將被設置為5,指向數組的第六個元素。

Limit

limit 變量表明還有多少數據需要取出(在從緩衝區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩衝區時)。

position 總是小於或者等於 limit

Capacity

緩衝區的 capacity 表明可以儲存在緩衝區中的最大數據容量。實際上,它指定了底層數組的大小 ― 或者至少是指定了准許我們使用的底層數組的容量。

limit 決不能大於 capacity

 

在實際操作數據時它們有如下關係圖:

① 新建一個大小為 8 個字節的緩衝區,此時 position 為 0,而 limit = capacity = 8。capacity 變量不會改變,下面的討論會忽略它。

② 從輸入通道中讀取 5 個字節數據寫入緩衝區中,此時 position 為 5,limit 保持不變。

③ 在將緩衝區的數據寫到輸出通道之前,需要先調用 flip() 方法,這個方法將 limit 設置為當前 position,並將 position 設置為 0。

④ 從緩衝區中取 4 個字節到輸出緩衝中,此時 position 設為 4。

⑤ 最後需要調用 clear() 方法來清空緩衝區,此時 position 和 limit 都被設置為最初位置。

 

文件複製 NIO 實例

以下展示了使用 NIO 快速複製文件的實例:

public static void fastCopy(String src, String dist) throws IOException {

    /* 獲得源文件的輸入字節流 */
    FileInputStream fin = new FileInputStream(src);

    /* 獲取輸入字節流的文件通道 */
    FileChannel fcin = fin.getChannel();

    /* 獲取目標文件的輸出字節流 */
    FileOutputStream fout = new FileOutputStream(dist);

    /* 獲取輸出字節流的文件通道 */
    FileChannel fcout = fout.getChannel();

    /* 為緩衝區分配 1024 個字節 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {

        /* 從輸入通道中讀取數據到緩衝區中 */
        int r = fcin.read(buffer);

        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }

        /* 切換讀寫 */
        buffer.flip();

        /* 把緩衝區的內容寫入輸出文件中 */
        fcout.write(buffer);

        /* 清空緩衝區 */
        buffer.clear();
    }
}

選擇器

NIO 常常被叫做非阻塞 IO,主要是因為 NIO 在網絡通信中的非阻塞特性被廣泛使用。

NIO 實現了 IO 多路復用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 通過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個線程就可以處理多個事件。

通過配置監聽的通道 Channel 為非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀態一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。

例如,當多個客戶端通過通道向服務端傳輸數據時,是通過 ByteBuffer 來傳輸,一個文件通過多次,從輸入通道中讀取 N 個字節數據寫入ByteBuffer,然後再將將緩衝區的數據寫到輸出通道,這個過程可以看成是不連續的,因為只有當緩衝區寫滿后,通過 buffer.flip() 切換成讀模式后,才開始向輸出通道寫入,所以當ByteBuffer還在寫入狀態時,服務器是不會等待這個通道的ByteBuffer寫滿,而是去處理其他客戶端Channel 為可讀的狀態,當然這個處理業務的工作可以開啟多線程來處理。

因為創建和切換線程的開銷很大,因此使用一個線程來處理多個事件而不是一個線程處理一個事件,對於 IO 密集型的應用具有很好地性能。

應該注意的是,只有套接字 Channel 才能配置為非阻塞,而 FileChannel 不能,為 FileChannel 配置非阻塞也沒有意義。

套接字 NIO 實例

package com.chenhao.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

/*
 * 一、使用 NIO 完成網絡通信的三個核心:
 * 
 * 1. 通道(Channel):負責連接
 *         
 *        java.nio.channels.Channel 接口:
 *             |--SelectableChannel
 *                 |--SocketChannel
 *                 |--ServerSocketChannel
 *                 |--DatagramChannel
 * 
 * 2. 緩衝區(Buffer):負責數據的存取
 * 
 * 3. 選擇器(Selector):是 SelectableChannel 的多路復用器。用於監控 SelectableChannel 的 IO 狀況
 * 
 */
public class TestNonBlockingNIO {
    
    //客戶端
    @Test
    public void client() throws IOException{
        //1. 獲取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        //2. 切換非阻塞模式
        sChannel.configureBlocking(false);
        
        //3. 分配指定大小的緩衝區
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //4. 發送數據給服務端
        Scanner scan = new Scanner(System.in);
        
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        
        //5. 關閉通道
        sChannel.close();
    }

    //服務端
    @Test
    public void server() throws IOException{
        //1. 獲取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        
        //2. 切換非阻塞模式
        ssChannel.configureBlocking(false);
        
        //3. 綁定連接
        ssChannel.bind(new InetSocketAddress(9898));
        
        //4. 獲取選擇器
        Selector selector = Selector.open();
        
        //5. 將通道註冊到選擇器上, 並且指定“監聽接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        //6. 輪詢式的獲取選擇器上已經“準備就緒”的事件
        //使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。
        while(selector.select() > 0){
            
            //7. 獲取當前選擇器中所有註冊的“選擇鍵(已就緒的監聽事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            
            while(it.hasNext()){
                //8. 獲取準備“就緒”的是事件
                SelectionKey sk = it.next();
                
                //9. 判斷具體是什麼事件準備就緒
                if(sk.isAcceptable()){
                    //10. 若“接收就緒”,獲取客戶端連接
                    SocketChannel sChannel = ssChannel.accept();
                    
                    //11. 切換非阻塞模式
                    sChannel.configureBlocking(false);
                    
                    //12. 將該通道註冊到選擇器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    //13. 獲取當前選擇器上“讀就緒”狀態的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    
                    //14. 讀取數據
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    
                    int len = 0;
                    while((len = sChannel.read(buf)) > 0 ){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                
                //15. 取消選擇鍵 SelectionKey
                //每一個“事件關鍵字”被處理后都必須移除,否則下一次輪詢時,這個事件會被重複處理
                it.remove();
            }
        }
    }
}

NIO傳輸文件

服務器端代碼

public class Server {
    private ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
        //使用Map保存每個連接,當OP_READ就緒時,根據key找到對應的文件對其進行寫入。若將其封裝成一個類,作為值保存,可以再上傳過程中显示進度等等
    Map<SelectionKey, FileChannel> fileMap = new HashMap<SelectionKey, FileChannel>();
    public static void main(String[] args) throws IOException{
        Server server = new Server();
        server.startServer();
    }
    public void startServer() throws IOException{
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(8888));
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服務器已開啟...");
        while (true) {
            int num = selector.select();
            if (num == 0) continue;
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel1 = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverChannel1.accept();
                    if (socketChannel == null) continue;
                    socketChannel.configureBlocking(false);
                    SelectionKey key1 = socketChannel.register(selector, SelectionKey.OP_READ);
                    InetSocketAddress remoteAddress = (InetSocketAddress)socketChannel.getRemoteAddress();
                    File file = new File(remoteAddress.getHostName() + "_" + remoteAddress.getPort() + ".txt");
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    fileMap.put(key1, fileChannel);
                    System.out.println(socketChannel.getRemoteAddress() + "連接成功...");
                    writeToClient(socketChannel);
                }
                else if (key.isReadable()){
                    readData(key);
                }
                // NIO的特點只會累加,已選擇的鍵的集合不會刪除,ready集合會被清空
                // 只是臨時刪除已選擇鍵集合,當該鍵代表的通道上再次有感興趣的集合準備好之後,又會被select函數選中
                it.remove();
            }
        }
    }
    private void writeToClient(SocketChannel socketChannel) throws IOException {
        buffer.clear();
        buffer.put((socketChannel.getRemoteAddress() + "連接成功").getBytes());
        buffer.flip();
        socketChannel.write(buffer);
        buffer.clear();
    }
    private void readData(SelectionKey key) throws IOException  {
        FileChannel fileChannel = fileMap.get(key);
        buffer.clear();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int num = 0;
        try {
            while ((num = socketChannel.read(buffer)) > 0) {
                buffer.flip();
                // 寫入文件
                fileChannel.write(buffer);
                buffer.clear();
                }
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
        // 調用close為-1 到達末尾
        if (num == -1) {
            fileChannel.close();
            System.out.println("上傳完畢");
            buffer.put((socketChannel.getRemoteAddress() + "上傳成功").getBytes());
            buffer.clear();
            socketChannel.write(buffer);
            key.cancel();
        }
    }
}

 

客戶端模擬三個客戶端同時向服務器發送文件

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            // 模擬三個發端
            new Thread() {
                public void run() {
                    try {
                        SocketChannel socketChannel = SocketChannel.open();
                        socketChannel.socket().connect(new InetSocketAddress("127.0.0.1", 8888));
                        File file = new File("E:\\" + 11 + ".txt");
                        FileChannel fileChannel = new FileInputStream(file).getChannel();
                        ByteBuffer buffer = ByteBuffer.allocate(100);
                        socketChannel.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
                        buffer.clear();
                        int num = 0;
                        while ((num=fileChannel.read(buffer)) > 0) {
                            buffer.flip();                        
                            socketChannel.write(buffer);
                            buffer.clear();
                        }
                        if (num == -1) {
                            fileChannel.close();
                            socketChannel.shutdownOutput();
                        }
                        // 接受服務器
                        socketChannel.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
                        buffer.clear();
                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    
                };
            }.start();
            
        }
        Thread.yield();
    }
}

可見這裏我們僅僅使用了一個線程就管理了三個連接,相比以前使用阻塞的Socket要在accept函數返回后開啟線程來管理這個連接,而使用NIO我們在accept返回后,僅僅將其註冊到選擇器上,讀操作在下次檢測到有可讀的鍵的集合時就會去處理。

NIO+線程池改進

public class ThreadPoolServer extends Server{
    private ExecutorService exec = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws IOException {
        ThreadPoolServer server = new ThreadPoolServer();
        server.startServer();
    }

    @Override
    protected void readData(final SelectionKey key) throws IOException {
        // 移除掉這個key的可讀事件,已經在線程池裡面處理,如果不改變當前Key的狀態,這裏交給另外一個線程去處理,主線程下一次遍歷此KEY還是可讀事件,會重複開啟線程處理任務
        key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
        exec.execute(new Runnable() {
            @Override
            public void run() {
                ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
                FileChannel fileChannel = fileMap.get(key);
                buffer.clear();
                SocketChannel socketChannel = (SocketChannel) key.channel();
                int num = 0;
                try {
                    while ((num = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        // 寫入文件
                        fileChannel.write(buffer);
                        buffer.clear();
                    }
                } catch (IOException e) {
                    key.cancel();
                    e.printStackTrace();
                    return;
                }
                // 調用close為-1 到達末尾
                if (num == -1) {
                    try {
                        fileChannel.close();
                        System.out.println("上傳完畢");
                        buffer.put((socketChannel.getRemoteAddress() + "上傳成功").getBytes());
                        buffer.clear();
                        socketChannel.write(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // 只有調用cancel才會真正從已選擇的鍵的集合裏面移除,否則下次select的時候又能得到
                    // 一端close掉了,其對端仍然是可讀的,讀取得到EOF,返回-1
                    key.cancel(); 
                    return;
                }
                // Channel的read方法可能返回0,返回0並不一定代表讀取完了。
                // 工作線程結束對通道的讀取,需要再次更新鍵的ready集合,將感興趣的集合重新放在裏面
                key.interestOps(key.interestOps() | SelectionKey.OP_READ);
                // 調用wakeup,使得選擇器上的第一個還沒有返回的選擇操作立即返回即重新select
                key.selector().wakeup();
            }
        });
    }
}

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

多路復用IO的優缺點

  • 不用再使用多線程來進行IO處理了(包括操作系統內核IO管理模塊和應用程序進程而言)。當然實際業務的處理中,應用程序進程還是可以引入線程池技術的
  • 同一個端口可以處理多種協議,例如,使用ServerSocketChannel的服務器端口監聽,既可以處理TCP協議又可以處理UDP協議。
  • 操作系統級別的優化:多路復用IO技術可以是操作系統級別在一個端口上能夠同時接受多個客戶端的IO事件。同時具有之前我們講到的阻塞式同步IO和非阻塞式同步IO的所有特點。Selector的一部分作用更相當於“輪詢代理器”。

【精選推薦文章】

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

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

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

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

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

雲遊戲流媒體整體架構設計(雲遊戲流媒體技術前瞻,最近雲遊戲概念很火,加之對流媒體技術略有研究,簡單寫一些)

前言:

遙想當年阿法狗戰敗一眾圍棋國手,風氣一轉,似乎所有人都懂AI。這次谷歌又放出了stadia,國內鵝廠再次跑步進場,貴州某xx雲提前布局。

閑來無事,嘗試體驗了一下貴州某xx雲的雲遊戲(不打廣告),暫且不評論如何如何,剛好對流媒體技術略有研究,僅在這裏簡單聊一下這方面涉及的架構和技術。

架構設計:

總體架構自上而下大致分為四端:

1、雲遊戲主機端(雲遊戲運行端,或者叫雲遊戲畫面渲染端,需要接收控制指令並錄屏推流到流媒體服務)

主機端需要運行遊戲並讓通過錄屏推流程序把渲染好的遊戲畫面(其實就是錄屏)推流到流媒體服務進行實時視頻分發。

有人會想這個雲遊戲主機端可能會很複雜,其實也還好,只是包含了錄屏、推流、用戶控制指令接收和一些其他諸如計費此類的相關功能。

2、流媒體服務(用於轉發主機端推上來的遊戲實時視頻並分發出去,所有用戶都可以觀看這個視頻)

這個不需要多講了,只是用來轉發遊戲實時視頻,並不涉及雲遊戲主機的控制權。

3、控制指令轉發服務(用戶需要獲取控制指令服務的所有權才能控制雲遊戲主機)

這個是雲遊戲的控制核心,獲取某台雲遊戲主機的用戶就可以通過鍵盤或者鼠標進行雲遊戲的試玩(操作),理論上講能夠獲取該控制權的不是只有一個用戶,完全可以支持多個用戶同時控制一台雲遊戲主機。

4、客戶端(瀏覽器,pc客戶端,ios,安卓客戶端等)

客戶端需要從流媒體服務拉取實時遊戲視頻,用戶需要先獲取雲遊戲主機的控制權,才能夠發送控制指令來試玩(操作)雲遊戲(鼠標,鍵盤,手柄等)

靈魂畫師繪製結構圖:

架構示意圖-來自靈魂畫師的傾情手繪

難點或者叫待解決的點:

1、流媒體協議的選擇?高延遲才是最大殺手

從流媒體技術出身開始,實時視頻延遲一直是個比較棘手的問題,比如rtmp/http-flv等基於tcp的協議本身優化到極點也要幾百毫秒的延遲,hls這種超高延遲到幾秒的不提也罷。就目前看只有sip、rtsp以及基於udp的一些協議能夠滿足這種超低延遲的需求,但是這種協議就很難在瀏覽器上就很難實現了,除了webrtc,而webrtc協議是谷歌力推的下一代流媒體協議,不排除這次是谷歌webrtc技術的奠基之作,拭目以待。

2、雲遊戲主機控制指令的所有權?依然是延遲

這個所有權其實不算是難點,只是用戶獲取某台雲遊戲主機的控制權而已。難點在於控制指令的延遲,沒錯,就是網絡延遲。尤其是在拉取實時視頻時,在視頻已經佔用大量帶寬的情況下,在這種網絡負載或者網絡波動較大的情況下控制指令延遲或許值得重視。

跟很多朋友討論過雲遊戲這個話題,不約而同第一個想到的都是網絡延遲,當然這個延遲不僅包含控制指令的延遲也指實時遊戲視頻的延遲。

 

後言(啰嗦幾句):

其實這塊依然屬於共享經濟的後續,類似共享單車。

給大家舉個栗子:我有一百台性能強勁的遊戲主機,每台主機價值一萬,當二手貨賣掉可能還會虧點,好可惜。那麼我把他共享出來,假設現在有十萬個用戶想租我這一百台機器,然後每人只收10塊錢月租,不考慮電費等其他成本,請問我什麼時候能回本?

作者:eguid

說明:原CSDN相關博客文章已經全部轉移到博客園

【精選推薦文章】

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

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

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

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