009.OpenShift管理及監控

一 資源限制

1.1 pod資源限制

pod可以包括資源請求和資源限制:

  • 資源請求

用於調度,並控制pod不能在計算資源少於指定數量的情況下運行。調度程序試圖找到一個具有足夠計算資源的節點來滿足pod請求。

  • 資源限制

用於防止pod耗盡節點的所有計算資源,基於pod的節點配置Linux內核cgroups特性,以執行pod的資源限制。

儘管資源請求和資源限制是pod定義的一部分,但通常建議在dc中設置。OpenShift推薦的實踐規定,不應該單獨創建pod,而應該由dc創建。

1.2 應用配額

OCP可以執行跟蹤和限制兩種資源使用的配額:

對象的數量:Kubernetes資源的數量,如pod、service和route。

計算資源:物理或虛擬硬件資源的數量,如CPU、內存和存儲容量。

通過避免master的Etcd數據庫的無限制增長,對Kubernetes資源的數量設置配額有助於OpenShift master服務器的穩定性。對Kubernetes資源設置配額還可以避免耗盡其他有限的軟件資源,比如服務的IP地址。

同樣,對計算資源的數量施加配額可以避免耗盡OpenShift集群中單個節點的計算能力。還避免了一個應用程序使用所有集群容量,從而影響共享集群的其他應用程序。

OpenShift通過使用ResourceQuota對象或簡單的quota來管理對象使用的配額及計算資源。

ResourceQuota對象指定項目的硬資源使用限制。配額的所有屬性都是可選的,這意味着任何不受配額限制的資源都可以無限制地使用。

注意:一個項目可以包含多個ResourceQuota對象,其效果是累積的,但是對於同一個項目,兩個不同的 ResourceQuota 不會試圖限制相同類型的對象或計算資源。

1.3 ResourceQuota限制資源

下錶显示 ResourceQuota 可以限制的主要對象和計算資源:

對象名 描述
pods 總計的pod數量
replicationcontrollers 總計的rc數量
services 總計的service數量
secrets 總計的secret數量
persistentvolumeclaims 總計的pvc數量
cpu 所有容器的CPU使用總量
memory 所有容器的總內存使用
storage 所有容器的磁盤總使用量

Quota屬性可以跟蹤項目中所有pod的資源請求或資源限制。默認情況下,配額屬性跟蹤資源請求。要跟蹤資源限制,可以在計算資源名稱前面加上限制,例如limit.cpu。

示例一:使用YAML語法定義的ResourceQuota資源,它為對象計數和計算資源指定了配額:

  1 $ cat
  2 apiVersion: v1
  3 kind: ResourceQuota
  4 metadata:
  5   name: dev-quota
  6 spec:
  7   hard:
  8     services: "10"
  9     cpu: "1300m"
 10     memory: "1.5Gi"
 11 $ oc create -f dev-quota.yml

示例二:使用oc create quota命令創建:

  1 $ oc create quota dev-quota \
  2 --hard=services=10 \
  3 --hard=cpu=1300m \
  4 --hard=memory=1.5Gi
  5 $ oc get resourcequota				#列出可用的配額
  6 $ oc describe resourcequota NAME		#查看與配額中定義的任何與限制相關的統計信息
  7 $ oc delete resourcequota compute-quota		#按名稱刪除活動配額

提示:若oc describe resourcequota命令不帶參數,只显示項目中所有resourcequota對象的累積限制集,而不显示哪個對象定義了哪個限制。

當在項目中首次創建配額時,項目將限制創建任何可能超出配額約束的新資源的能力,然後重新計算資源使用情況。在創建配額和使用數據統計更新之後,項目接受新內容的創建。當創建新資源時,配額使用量立即增加。當一個資源被刪除時,在下一次對項目的 quota 統計數據進行全面重新計算時,配額使用將減少。

ResourceQuota 應用於整個項目,但許多 OpenShift 過程,例如 build 和 deployment,在項目中創建 pod,可能會失敗,因為啟動它們將超過項目 quota。

如果對項目的修改超過了對象數量的 quota,則服務器將拒絕操作,並向用戶返回錯誤消息。但如果修改超出了計算資源的quota,則操作不會立即失敗。OpenShift 將重試該操作幾次,使管理員有機會增加配額或執行糾正操作,比如上線新節點,擴容節點資源。

注意:如果設置了計算資源的 quota,OpenShift 拒絕創建不指定該計算資源的資源請求或資源限制的pod。

1.3 應用限制範圍

LimitRange資源,也稱為limit,定義了計算資源請求的默認值、最小值和最大值,以及項目中定義的單個pod或單個容器的限制,pod的資源請求或限制是其容器的總和。

要理解limit rang和resource quota之間的區別,limit rang為單個pod定義了有效範圍和默認值,而resource quota僅為項目中所有pod的和定義了最高值。

通常可同時定義項目的限制和配額。

LimitRange資源還可以為image、is或pvc的存儲容量定義默認值、最小值和最大值。如果添加到項目中的資源不提供計算資源請求,那麼它將接受項目的limit ranges提供的默認值。如果新資源提供的計算資源請求或限制小於項目的limit range指定的最小值,則不創建該資源。同樣,如果新資源提供的計算資源請求或限制高於項目的limit range所指定的最大值,則不會創建該資源。

OpenShift 提供的大多數標準 S2I builder image 和 templabe 都沒有指定。要使用受配額限制的 template 和 builder,項目需要包含一個 limit range 對象,該對象為容器資源請求指定默認值。

如下為描述了一些可以由LimitRange對象指定的計算資源。


類型 資源名稱 描述
Container cpu 每個容器允許的最小和最大CPU。
Container memory 每個容器允許的最小和最大內存
Pod cpu 一個pod中所有容器允許的最小和最大CPU
Pod memory 一個pod中所有容器允許的最小和最大內存
Image storage 可以推送到內部倉庫的圖像的最大大小
PVC storage 一個pvc的容量的最小和最大容量

示例一:limit rang的yaml示例:

  1 $ cat dev-limits.yml
  2 apiVersion: "v1"
  3 kind: "LimitRange"
  4 metadata:
  5   name: "dev-limits"
  6 spec:
  7   limits:
  8     - type: "Pod"
  9       max:
 10         cpu: "2"
 11         memory: "1Gi"
 12       min:
 13         cpu: "200m"
 14         memory: "6Mi"
 15     - type: "Container"
 16       default:
 17         cpu: "1"
 18         memory: "512Mi"
 19 $ oc create -f dev-limits.yml
 20 $ oc describe limitranges NAME		#查看項目中強制執行的限制約束
 21 $ oc get limits				#查看項目中強制執行的限制約束
 22 $ oc delete limitranges name		#按名稱刪除活動的限制範圍

提示:OCP 3.9不支持使用oc create命令參數形式創建limit rang。

在項目中創建limit rang之後,將根據項目中的每個limit rang資源評估所有資源創建請求。如果新資源違反由任何limit rang設置的最小或最大約束,則拒絕該資源。如果新資源沒有聲明配置值,且約束支持默認值,則將默認值作為其使用值應用於新資源。

所有資源更新請求也將根據項目中的每個limit rang資源進行評估,如果更新后的資源違反了任何約束,則拒絕更新。

注意:避免將LimitRange設的過高,或ResourceQuota設的過低。違反LimitRange將阻止pod創建,並清晰保存。違反ResourceQuota將阻止pod被調度,狀態轉為pending。

1.4 多項目quota配額

ClusterResourceQuota資源是在集群級別創建的,創建方式類似持久卷,並指定應用於多個項目的資源約束。

可以通過以下兩種方式指定哪些項目受集群資源配額限制:

  • 使用openshift.io/requester標記,定義項目所有者,該所有者的所有項目均應用quota;
  • 使用selector,匹配該selector的項目將應用quota。

示例1:

  1 $ oc create clusterquota user-qa \
  2 --project-annotation-selector openshift.io/requester=qa \
  3 --hard pods=12 \
  4 --hard secrets=20			#為qa用戶擁有的所有項目創建集群資源配額
  5 $ oc create clusterquota env-qa \
  6 --project-label-selector environment=qa \
  7 --hard pods=10 \
  8 --hard services=5			#為所有具有environment=qa標籤的項目創建集群資源配額
  9 $ oc describe QUOTA NAME		#查看應用於當前項目的集群資源配額
 10 $ oc delete clusterquota NAME		#刪除集群資源配額

提示:不建議使用一個集群資源配額來匹配超過100個項目。這是為了避免較大的locking開銷。當創建或更新項目中的資源時,在搜索所有適用的資源配額時鎖定項目需要較大的資源消耗。

二 限制資源使用

2.1 前置準備

準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

2.2 本練習準備

  1 [student@workstation ~]$ lab monitor-limit setup

2.3 查看當前資源

  1 [student@workstation ~]$ oc login -u admin -p redhat https://master.lab.example.com
  2 [student@workstation ~]$ oc describe node node1.lab.example.com | grep -A 4 Allocated
  3 [student@workstation ~]$ oc describe node node2.lab.example.com | grep -A 4 Allocated

2.4 創建應用

  1 [student@workstation ~]$ oc new-project resources
  2 [student@workstation ~]$ oc new-app --name=hello \
  3 --docker-image=registry.lab.example.com/openshift/hello-openshift
  4 [student@workstation ~]$ oc get pod -o wide
  5 NAME            READY     STATUS    RESTARTS   AGE       IP            NODE
  6 hello-1-znk56   1/1       Running   0          24s       10.128.0.16   node1.lab.example.com

2.5 刪除應用

  1 [student@workstation ~]$ oc delete all -l app=hello

2.6 添加資源限制

作為集群管理員,向項目quota和limit range,以便為項目中的pod提供默認資源請求。

  1 [student@workstation ~]$ cd /home/student/DO280/labs/monitor-limit/
  2 [student@workstation monitor-limit]$ cat limits.yml		#創建limit range
  3 apiVersion: "v1"
  4 kind: "LimitRange"
  5 metadata:
  6   name: "project-limits"
  7 spec:
  8   limits:
  9     - type: "Container"
 10       default:
 11         cpu: "250m
 12 [student@workstation monitor-limit]$ oc create -f limits.yml	#創建limit range
 13 [student@workstation monitor-limit]$ oc describe limitrange	#查看limit range

  1 [student@workstation monitor-limit]$ cat quota.yml		#創建配額
  2 apiVersion: v1
  3 kind: ResourceQuota
  4 metadata:
  5   name: project-quota
  6 spec:
  7   hard:
  8     cpu: "900m"
  9 [student@workstation monitor-limit]$ oc create -f quota.yml
 10 [student@workstation monitor-limit]$ oc describe quota		#確保創建了resource限制

2.7 授權項目

  1 [student@workstation monitor-limit]$ oc adm policy add-role-to-user edit developer

2.8 驗證資源限制

  1 [student@workstation ~]$ oc login -u developer -p redhat https://master.lab.example.com
  2 [student@workstation ~]$ oc project resources			#選擇項目
  3 Already on project "resources" on server "https://master.lab.example.com:443".
  4 [student@workstation ~]$ oc get limits				#查看limit
  5 NAME             AGE
  6 project-limits   14m
  7 [student@workstation ~]$ oc delete limits project-limits	#驗證限制是否有效,但developer用戶不能刪除該限制
  8 Error from server (Forbidden): limitranges "project-limits" is forbidden: User "developer" cannot delete limitranges in the namespace "resources": User "developer" cannot delete limitranges in project "resources"
  9 [student@workstation ~]$ oc get quota
 10 NAME            AGE
 11 project-quota   15m

2.9 創建應用

  1 [student@workstation ~]$ oc new-app --name=hello \
  2 --docker-image=registry.lab.example.com/openshift/hello-openshift
  3 [student@workstation ~]$ oc get pod
  4 NAME            READY     STATUS    RESTARTS   AGE
  5 hello-1-t7tfn   1/1       Running   0          35s

2.10 查看quota

  1 [student@workstation ~]$ oc describe quota
  2 Name:       project-quota
  3 Namespace:  resources
  4 Resource    Used  Hard
  5 --------    ----  ----
  6 cpu         250m  900m

2.11 查看節點可用資源

  1 [student@workstation ~]$ oc login -u admin -p redhat \
  2 https://master.lab.example.com
  3 [student@workstation ~]$ oc get pod -o wide -n resources
  4 [student@workstation ~]$ oc describe node node1.lab.example.com | grep -A 4 Allocated
  5 [student@workstation ~]$ oc describe pod hello-1-t7tfn | grep -A2 Requests

2.12 擴容應用

  1 [student@workstation ~]$ oc scale dc hello --replicas=2		#擴容應用
  2 [student@workstation ~]$ oc get pod				#查看擴容后的pod
  3 [student@workstation ~]$ oc describe quota			#查看擴容后的quota情況
  4 [student@workstation ~]$ oc scale dc hello --replicas=4		#繼續擴容至4個
  5 [student@workstation ~]$ oc get pod				#查看擴容的pod
  6 [student@workstation ~]$ oc describe dc hello | grep Replicas	#查看replaces情況

結論:由於超過了配額規定,會提示控制器無法創建第四個pod。

2.13 添加配額請求

  1 [student@workstation ~]$ oc scale dc hello --replicas=1
  2 [student@workstation ~]$ oc get pod
  3 [student@workstation ~]$ oc set resources dc hello --requests=memory=256Mi	#設置資源請求
  4 [student@workstation ~]$ oc get pod
  5 [student@workstation ~]$ oc describe pod hello-2-4jvpw | grep -A 3 Requests
  6 [student@workstation ~]$ oc describe quota					#查看quota

結論:由上可知從項目的配額角度來看,沒有什麼變化。

2.14 增大配額請求

  1 [student@workstation ~]$ oc set resources dc hello --requests=memory=8Gi	#將內存請求增大到超過node最大值
  2 [student@workstation ~]$ oc get pod						#查看pod
  3 [student@workstation ~]$ oc logs hello-3-deploy					#查看log
  4 [student@workstation ~]$ oc status

結論:由於資源請求超過node最大值,最終显示一個警告,說明由於內存不足,無法將pod調度到任何節點。

三 OCP升級

3.1 升級OPENSHIFT

當OCP的新版本發布時,可以升級現有集群,以應用最新的增強功能和bug修復。這包括從以前的次要版本(如從3.7升級到3.9)升級,以及對次要版本(3.7)應用更新。

提示:OCP 3.9包含了Kubernetes 1.8和1.9的特性和補丁的合併。由於主要版本之間的核心架構變化,OpenShift Enterprise 2環境無法升級為OpenShift容器平台3,必須需要重新安裝。

通常,主版本中不同子版本的node是向前和向後兼容的。但是,運行不匹配的版本的時間不應超過升級整個集群所需的時間。此外,不支持使用quick installer將版本3.7升級到3.9。

3.2 升級方式

有兩種方法可以執行OpenShift容器平台集群升級,一種為in-place升級(可以自動升級或手動升級),也可以使用blue-green部署方法進行升級。

in-place升級:使用此方式,集群升級將在單個運行的集群中的所有主機上執行。首先升級master,然後升級node。在node升級開始之前,Pods被遷移到集群中的其他節點。這有助於減少用戶應用程序的停機時間。

注意:對於使用quick和高級安裝方法安裝的集群,可以使用自動in-place方式升級。

當使用高級安裝方法安裝集群時,您可以通過重用它們的庫存文件執行自動化或手動就地升級。

blue-green部署:blue-green部署是一種旨在減少停機時間同時升級環境的方法。在blue-green部署中,相同的環境與一個活動環境一起運行,而另一個環境則被更新。OpenShift升級方法標記了不可調度節點,並將pod調度到可用節點。升級成功后,節點將恢復到可調度狀態。

3.3 執行自動化集群升級

使用高級安裝方法,可以使用Ansible playbook自動化執行OpenShift集群升級過程。用於升級的劇本位於/usr/share/ansible/openshift-ansible/Playbooks/common/openshift-cluster/updates/中。該目錄包含一組用於升級集群的子目錄,例如v3_9。

注意:將集群升級到 OCP 3.9 前,集群必須已經升級到 3.7。集群升級一次不能跨越一個以上的次要版本,因此,如果集群的版本早於3.6,則必須先漸進地升級,例如從3.5升級到3.6,然後從3.6升級到3.7

要執行升級,可以使用ansible-playbook命令運行升級劇本,如使用v3_9 playbook將運行3.7版本的現有OpenShift集群升級到3.9版本。

自動升級主要執行以下任務:

  • 應用最新的配置更改;
  • 保存Etcd數據;
  • 將api從3.7更新到3.8,然後從3.8更新到3.9;
  • 如果存在,將默認路由器從3.7更新到3.9;
  • 如果存在,則將默認倉庫從3.7更新到3.9;
  • 更新默認is和Templates。

注意:在繼續升級之前,確保已經滿足了所有先決條件,否則可能導致升級失敗。

如果使用容器化的GlusterFS,節點將不會從pod中撤離,因為GlusterFS pod作為daemonset的一部分運行。要正確地升級運行容器化GlusterFS的集群,需要:

1:升級master服務器、Etcd和基礎設施服務(route、內部倉庫、日誌記錄和metric)。

2:升級運行應用程序容器的節點。

3:一次升級一個運行GlusterFS的節點。

注意:在升級之前,使用oc adm diagnostics命令驗證集群的健康狀況。這確認節點處於ready狀態,運行預期的啟動版本,並且沒有診斷錯誤或警告。對於離線安裝,使用–network-pod-image=’REGISTRY URL/ IMAGE參數指定要使用的image。

3.4 準備自動升級

下面的過程展示了如何為自動升級準備環境,在執行升級之前,Red Hat建議檢查配置Inventory文件,以確保對Inventory文件進行了手動更新。如果配置沒有修改,則使用默認值覆蓋更改。

  1. 如果這是從OCP 3.7升級到3.9,手動禁用3.7存儲庫,並在每個master節點和node節點上啟用3.8和3.9存儲庫:
  1 [root@demo ~]# subscription-manager repos \
  2 --disable="rhel-7-server-ose-3.7-rpms" \
  3 --enable="rhel-7-server-ose-3.9-rpms" \
  4 --enable="rhel-7-server-ose-3.8-rpms" \
  5 --enable="rhel-7-server-rpms" \
  6 --enable="rhel-7-server-extras-rpms" \
  7 --enable="rhel-7-server-ansible-2.4-rpms" \
  8 --enable="rhel-7-fast-datapath-rpms"
  9 [root@demo ~]# yum clean all

  1. 確保在每個RHEL 7系統上都有最新版本的atom-openshift-utils包,它還更新openshift-ansible-*包。
  1 [root@demo ~]# yum update atomic-openshift-utils
  1. 在OpenShift容器平台的以前版本中,安裝程序默認將master節點標記為不可調度,但是,從OCP 3.9開始,master節點必須標記為可調度的,這是在升級過程中自動完成的。

如果沒有設置默認的節點選擇器(如下配置),它們將在升級過程中添加。則master節點也將被標記為master節點角色。所有其他節點都將標記為compute node角色。

  1 openshift_node_labels="{'region':'infra', 'node-role.kubernetes.io/compute':'true'}
  1. 如果將openshift_disable_swap=false變量添加到的Ansible目錄中,或者在node上手動配置swap,那麼在運行升級之前禁用swap內存。

3.5 升級master節點和node節點

在滿足了先決條件(如準備工作)之後,則可以按照如下步驟進行升級:

  1. 在清單文件中設置openshift_deployment_type=openshift-enterprise變量。
  2. 如果使用自定義Docker倉庫,則必須顯式地將倉庫的地址指定為openshift_web_console_prefix和template_service_broker_prefix變量。這些值由Ansible在升級過程中使用。
  1 openshift_web_console_prefix=registry.demo.example.com/openshift3/ose-
  2 template_service_broker_prefix=registry.demo.example.com/openshift3/ose-

  1. 如果希望重啟service或重啟node,請在Inventory文件中設置openshift_rolling_restart_mode=system選項。如果未設置該選項,則默認值表明升級過程在master節點上執行service重啟,但不重啟系統。
  2. 可以通過運行一個Ansible Playbook (upgrade.yml)來更新環境中的所有節點,也可以通過使用單獨的Playbook分多個階段進行升級。
  3. 重新啟動所有主機,重啟之後,如果沒有部署任何額外的功能,可以驗證升級。

3.6 分階段升級集群

如果決定分多個階段升級環境,根據Ansible Playbook (upgrade_control_plan .yml)確定的第一個階段,升級以下組件:

  1. master節點;
  2. 運行master節點的節點services;
  3. Docker服務位於master節點和任何獨立Etcd主機上。

第二階段由upgrade_nodes.yml playbook,升級了以下組件。在運行此第二階段之前,必須已經升級了master節點。

  1. node節點的服務;
  2. 運行在獨立節點上的Docker服務。

兩個階段的升級過程允許通過指定自定義變量自定義升級的運行方式。例如,要升級總節點的50%,可以運行以下命令:

  1 [root@demo ~]# ansible-playbook \
  2 /usr/share/ansible/openshift-ansible/playbooks/common/openshift-cluster/upgrades/
  3 v3_9/upgrade_nodes.yml \
  4 -e openshift_upgrade_nodes_serial="50%"

若要在HA region一次升級兩個節點,請運行以下命令:

  1 [root@demo ~]# ansible-playbook \
  2 /usr/share/ansible/openshift-ansible/playbooks/common/openshift-cluster/upgrades/
  3 v3_9/upgrade_nodes.yml \
  4 -e openshift_upgrade_nodes_serial="2"
  5 -e openshift_upgrade_nodes_label="region=HA"

要指定每個更新批處理中允許有多少節點失敗,可使用openshift_upgrade_nodes_max_fail_percent選項。當故障百分比超過定義的值時,Ansible將中止升級。

使用openshift_upgrade_nodes_drain_timeout選項指定中止play前等待的時間。

示例:如下所示一次升級10個節點,以及如果20%以上的節點(兩個節點)失敗,以及終止play執行的等待時間。

  1 [root@demo ~]# ansible-playbook \
  2 /usr/share/ansible/openshift-ansible/playbooks/common/openshift-cluster/upgrades/
  3 v3_9/upgrade_nodes.yml \
  4 -e openshift_upgrade_nodes_serial=10 \
  5 -e openshift_upgrade_nodes_max_fail_percentage=20 \
  6 -e openshift_upgrade_nodes_drain_timeout=600

3.7 使用Ansible Hooks

可以通過hook為特定的操作執行定製的任務。hook允許通過定義在升級過程中特定點之前或之後執行的任務來擴展升級過程的默認行為。例如,可以在升級集群時驗證或更新自定義基礎設施組件。

提示:hook沒有任何錯誤處理機制,因此,hook中的任何錯誤都會中斷升級過程。需要修復hook並重新運行升級過程。

使用Inventory文件的[OSEv3:vars]部分來定義hook。每個hook必須指向一個.yaml文件,該文件定義了可能的任務。該文件是作為include語句的一部分集成的,該語句要求定義一組任務,而不是一個劇本。Red Hat建議使用絕對路徑來避免任何歧義。

以下hook可用於定製升級過程:

1. openshift_master_upgrade_pre_hook:hook在更新每個master節點之前運行。

2. openshift_master_upgrade_hook:hook在每個master節點升級之後、主服務或節點重新啟動之前運行。

3.openshift_master_upgrade_post_hook:hook在每個master節點升級並重啟服務或系統之後運行。

示例:在庫存文件中集成一個鈎子。

  1 [OSEv3:vars]
  2 openshift_master_upgrade_pre_hook=/usr/share/custom/pre_master.yml
  3 openshift_master_upgrade_hook=/usr/share/custom/master.yml
  4 openshift_master_upgrade_post_hook=/usr/share/custom/post_master.yml

如上示例,引入了一個pre_master.yml,包括了以下任務:

  1 ---
  2 - name: note the start of a master upgrade
  3 debug:
  4 msg: "Master upgrade of {{ inventory_hostname }} is about to start"
  5 - name: require an operator agree to start an upgrade pause:
  6 prompt: "Hit enter to start the master upgrade"

3.8 驗證升級

升級完成后,應該執行以下步驟以確保升級成功。

  1 [root@demo ~]# oc get nodes		#驗證node處於ready
  2 [root@demo ~]# oc get -n default dc/docker-registry -o json | grep \"image\"
  3 #驗證倉庫版本
  4 [root@demo ~]# oc get -n default dc/router -o json | grep \"image\
  5 #驗證image版本
  6 [root@demo ~]# oc adm diagnostics	#使用診斷工具

3.9 升級步驟匯總

  1. 確保在每個RHEL 7系統上都有atom-openshift-utils包的最新版本。
  2. 如果使用自定義Docker倉庫,可以選擇將倉庫的地址指定為openshift_web_console_prefix和template_service_broker_prefix變量。
  3. 禁用所有節點上的swap。
  4. 重新啟動所有主機,重啟之後,檢查升級。
  5. 可選地:檢查Inventory文件中的節點選擇器。
  6. 禁用3.7存儲庫,並在每個master主機和node節點主機上啟用3.8和3.9存儲庫。
  7. 通過使用合適的Ansible劇本集,使用單個或多個階段策略進行更新。
  8. 在清單文件中設置openshift_deployment_type=openshift-enterprise變量。

四 使用probes監視應用

4.1 OPENSHIFT探針介紹

OpenShift應用程序可能會因為臨時連接丟失、配置錯誤或應用程序錯誤等問題而異常。開發人員可以使用探針來監視他們的應用程序。探針是一種Kubernetes操作,它定期對正在運行的容器執行診斷。可以使用oc客戶端命令或OpenShift web控制台配置探針。

目前,可以使用兩種類型的探測:

  • Liveness探針

Liveness探針確定在容器中運行的應用程序是否處於健康狀態。如果Liveness探針返回檢測到一個不健康的狀態,OpenShift將殺死pod並試圖重新部署它。開發人員可以通過配置template.spec.container.livenessprobe來設置Liveness探針。

  • Readiness探針

Readiness探針確定容器是否準備好為請求服務,如果Readiness探針返回失敗狀態,OpenShift將從所有服務的端點刪除容器的IP地址。開發人員可以使用Readiness探針向OpenShift發出信號,即使容器正在運行,它也不應該從代理接收任何流量。開發人員可以通過配置template.spec.containers.readinessprobe來設置Readiness探針。

OpenShift為探測提供了許多超時選項,有五個選項控制支持如上兩個探針:

initialDelaySeconds:強制性的。確定容器啟動后,在開始探測之前要等待多長時間。

timeoutSeconds:強制性的確定等待探測完成所需的時間。如果超過這個時間,OpenShift容器平台會認為探測失敗。

periodSeconds:可選的,指定檢查的頻率。

successThreshold:可選的,指定探測失敗后被認為成功的最小連續成功數。

failureThreshold:可選的,指定探測器成功后被認為失敗的最小連續故障。

4.2 檢查應用程序健康

Readiness和liveness probes可以通過三種方式檢查應用程序的健康狀況:

HTTP檢查:當使用HTTP檢查時,OpenShift使用一個webhook來確定容器的健康狀況。如果HTTP響應代碼在200到399之間,則認為檢查成功。

示例:演示如何使用HTTP檢查方法實現readiness probe 。

  1 ...
  2 readinessProbe:
  3   httpGet:
  4     path: /health			#檢測的URL
  5     port: 8080				#端口
  6   initialDelaySeconds: 15		#在容器啟動后多久才能檢查其健康狀況
  7   timeoutSeconds: 1			#要等多久探測器才能完成
  8 ...

4.3 容器執行檢查

當使用容器執行檢查時,kubelet agent在容器內執行命令。退出狀態為0的檢查被認為是成功的。

示例:實現容器檢查。

  1 ...
  2 livenessProbe:
  3   exec:
  4     command:
  5     - cat
  6     - /tmp/health
  7   initialDelaySeconds: 15
  8   timeoutSeconds: 1
  9 ...

4.4 TCP Socket檢查

當使用TCP Socket檢查時,kubelet agent嘗試打開容器的socket。如果檢查能夠建立連接,則認為容器是健康的。

示例:使用TCP套接字檢查方法實現活動探測。

  1 ...
  2 livenessProbe:
  3   tcpSocket:
  4     port: 8080
  5   initialDelaySeconds: 15
  6   timeoutSeconds: 1
  7 ...

4.5 使用Web管理probes

開發人員可以使用OpenShift web控制台管理readiness和liveness探針。對於每個部署,探針管理都可以從Actions下拉列表中獲得。

對於每種探針類型,開發人員可以選擇該類型,例如HTTP GET、TCP套接字或命令,併為每種類型指定參數。web控制台還提供了刪除探針的選項。

web控制台還可以用於編輯定義部署配置的YAML文件。在創建探針之後,將一個新條目添加到DC的配置文件中。使用DC編輯器來檢查或編輯探針。實時編輯器允許編輯周期秒、成功閾值和失敗閾值選項。

五 使用探針監視應用程序實驗

5.1 前置準備

準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

5.2 本練習準備

  1 [student@workstation ~]$ lab probes setup

5.3 創建應用

  1 [student@workstation ~]$ oc login -u developer -p redhat \
  2 https://master.lab.example.com
  3 [student@workstation ~]$ oc new-project probes
  4 [student@workstation ~]$ oc new-app --name=probes \
  5 http://services.lab.example.com/node-hello
  6 [student@workstation ~]$ oc status

  1 [student@workstation ~]$ oc get pods -w
  2 NAME             READY     STATUS      RESTARTS   AGE
  3 probes-1-build   0/1       Completed   0          1m
  4 probes-1-nqpwh   1/1       Running     0          12s

5.4 暴露服務

  1 [student@workstation ~]$ oc expose svc probes --hostname=probe.apps.lab.example.com
  2 [student@workstation ~]$ curl http://probe.apps.lab.example.com
  3 Hi! I am running on host -> probes-1-nqpwh

5.5 檢查服務

  1 [student@workstation ~]$ curl http://probe.apps.lab.example.com/health
  2 OK
  3 [student@workstation ~]$ curl http://probe.apps.lab.example.com/ready
  4 READY

5.6 創建readiness探針

使用Web控制台登錄。並創建readiness探針。

Add readiness probe

參考5.5存在的用於檢查健康的鏈接添加probe。

5.7 創建Liveness探針

使用Web控制台登錄。並創建Liveness探針。

參考5.5存在的用於檢查健康,特意使用healtz錯誤的值而不是health創建,從而測試相關報錯。這個錯誤將導致OpenShift認為pod不健康,這將觸發pod的重新部署。

提示:由於探針更新了部署配置,因此更改將觸發一個新的部署。

5.8 確認探測

通過單擊側欄上的Monitoring查看探測的實現。觀察事件面板的實時更新。此時將標記為不健康的條目,這表明liveness探針無法訪問/healtz資源。

view details查看詳情。

[student@workstation ~]$ oc get events –sort-by=’.metadata.creationTimestamp’ | grep ‘probe failed’ #查看probe失敗事件

5.9 修正probe

修正healtz為health。

5.10 再次確認

  1 [student@workstation ~]$ oc get events \
  2 --sort-by='.metadata.creationTimestamp'

#從終端重新運行oc get events命令,此時OpenShift在重新部署DC新版本,以及殺死舊pod。同時將不會有任何關於pod不健康的信息。

六 Web控制台使用

6.1 WEB控制台簡介

OpenShift web控制台是一個可以從web瀏覽器訪問的用戶界面。它是管理和監視應用程序的一種方便的方法。儘管命令行界面可以用於管理應用程序的生命周期,但是web控制台提供了額外的優勢,比如部署、pod、服務和其他資源的狀態,以及

關於系統範圍內事件的信息。

可使用Web查看基礎設施內的重要信息,包括:

  • pod各種狀態;
  • volume的可用性;
  • 通過使用probes獲得應用程序的健康行;

登錄並選擇項目之後,web控制台將提供項目項目的概述。

  1. 項目允許在授權訪問的項目之間切換。
  2. Search Catalog:瀏覽image目錄。
  3. Add to project:向項目添加新的資源和應用程序。可以從文件或現有項目導入資源。
  4. Overview:提供當前項目的高級視圖。它显示service的名稱及其在項目中運行的相關pod。
  5. Applications:提供對部署、pod、服務和路由的訪問。它還提供了對Stateful set的訪問,Kubernetes hat特性為pod提供了一個惟一的標識,用於管理部署的順序。
  6. build:提供對構建和IS的訪問。
  7. Resources:提供對配額管理和各種資源(如角色和端點)的訪問。
  8. Storage:提供對持久卷和存儲請求的訪問。
  9. Monitoring選項卡提供對構建、部署和pod日誌的訪問。它還提供了對項目中各種對象的事件通知的訪問。
  10. Catalog選項卡提供對可用於部署應用程序包的模板的訪問。

6.2 使用HAWKULAR管理指標

Hawkular是一組用於監控環境的開源項目。它由各種組件組成,如Hawkular services、Hawkular Application Performance Management (APM)和Hawkular metrics。Hawkular可以通過Hawkular OpenShift代理在OpenShift集群中收集應用程序指標。通過在OpenShift集群中部署Hawkular,可以訪問各種指標,比如pod使用的內存、cpu數量和網絡使用情況。

在部署了Hawkular代理之後,web控制台可以查看各種pod的圖表了。

6.3 管理Deployments和Pods

·Actions按鈕可用於pod和部署,允許管理各種設置。例如,可以向部署添加存儲或健康檢查(包括準備就緒和活動探測)。該按鈕還允許訪問YAML編輯器,以便通過web控制台實時更新配置。

6.4 管理存儲

web控制台允許訪問存儲管理,可以使用該接口創建卷聲明,以使用向項目公開的卷。注意,該接口不能用於創建持久卷,因為只有管理員才能執行此任務。管理員創建持久性卷之後,可以使用web控制台創建請求。該接口支持使用選擇器和標籤屬性。

定義卷聲明之後,控制台將显示它所使用的持久性卷,這是由管理員定義的。、

七 Web控制台監控指標

7.1 前置準備

準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

同時安裝OpenShift Metrics,參考《008.OpenShift Metric應用》3.1

7.2 本練習準備

  1 [student@workstation ~]$ lab web-console setup

7.3 創建項目

  1 [student@workstation ~]$ oc login -u developer -p redhat \
  2 https://master.lab.example.com
  3 [student@workstation ~]$ oc new-project load
  4 [student@workstation ~]$ oc new-app --name=load http://services.lab.example.com/node-hello

7.4 ·暴露服務

  1 [student@workstation ~]$ oc expose svc load
  2 [student@workstation ~]$ oc get pod
  3 NAME           READY     STATUS    RESTARTS   AGE
  4 load-1-build   1/1       Running   0          48s

7.5 壓力測試

  1 [student@workstation ~]$ sudo yum install httpd-tools
  2 [student@workstation ~]$ ab -n 3000000 -c 20 \
  3 http://load-load.apps.lab.example.com/

7.6 控制台擴容pod

workstation節點上登錄控制填,並擴展應用。

查看概覽頁面,確保有一個pod在運行。單擊部署配置load #1,所显示的第一個圖,它對應於pod使用的內存。並指示pod使用了多少內存,突出显示第二張圖,該圖表示pods使用的cpu數量。突出显示第三個圖,它表示pod的網絡流量。

單擊pod視圖圈旁的向上指向的箭頭,將此應用程序的pod數量增加到兩個。

導航到應用程序→部署以訪問項目的部署

注意右側的Actions按鈕,單擊它並選擇Edit YAML來編輯部署配置。

檢查部署的YAML文件,確保replicas條目的值為2,該值與為該部署運行的pod的數量相匹配。

7.7 查看metric

單擊Metrics選項卡訪問項目的度量,可以看到應用程序的四個圖:使用的內存數量、使用的cpu數量、接收的網絡數據包數量和發送的網絡數據包數量。對於每個圖,有兩個圖,每個圖被分配到一個pod。

7.8 查看web控制監視

在側窗格中,單擊Monitoring以訪問Monitoring頁面。Pods部分下應該有兩個條目,deployment部分下應該有一個條目。

向下滾動以訪問部署,並單擊部署名稱旁邊的箭頭以打開框架。日誌下面應該有三個圖表:一個表示pod使用的內存數量,一個表示pod使用的cpu數量,一個表示pod發送和接收的網絡數據包。

7.9 創建PV

為應用程序創建PVC,此練習環境已經提供了聲明將綁定到的持久卷。

單擊Storage創建持久卷聲明,單擊Create Storage來定義聲明。輸入web-storage作為名稱。選擇Shared Access (RWX)作為訪問模式。輸入1作為大小,並將單元保留為GiB

單擊Create創建持久卷聲明。

7.10 嚮應用程序添加存儲

導航到應用程序——>部署來管理部署,單擊load條目以訪問部署。單擊部署的Actions,然後選擇Add Storage選項。此選項允許將現有的持久卷聲明添加到部署配置的模板中。選擇web-storage作為存儲聲明,輸入/web-storage作為掛載路徑,web-storage作為卷名。

7.11 檢查存儲

從deployment頁面中,單擊由(latest)指示的最新部署。等待兩個副本被標記為活動的。確保卷部分將卷web存儲作為持久卷。從底部的Pods部分中,選擇一個正在運行的Pods。單擊Terminal選項卡打開pod的外殼。

也可在任何一個pod中運行如下命令查看:

八 管理和監控OpenShift

8.1 前置準備

準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

8.2 本練習準備

  1 [student@workstation ~]$ lab review-monitor setup

8.3 創建項目

  1 [student@workstation ~]$ oc login -u developer -p redhat https://master.lab.example.com、
  2 [student@workstation ~]$ oc new-project load-review

8.4 創建limit range

  1 [student@workstation ~]$ oc login -u admin -p redhat
  2 [student@workstation ~]$ oc project load-review
  3 [student@workstation ~]$ cat /home/student/DO280/labs/monitor-review/limits.yml
  4 apiVersion: "v1"
  5 kind: "LimitRange"
  6 metadata:
  7   name: "review-limits"
  8 spec:
  9   limits:
 10     - type: "Container"
 11       max:
 12         memory: "300Mi"
 13       default:
 14         memory: "200Mi"
 15 [student@workstation ~]$ oc create -f /home/student/DO280/labs/monitor-review/limits.yml
 16 [student@workstation ~]$ oc describe limitrange
 17 Name:       review-limits
 18 Namespace:  load-review
 19 Type        Resource  Min  Max    Default Request  Default Limit  Max Limit/Request Ratio
 20 ----        --------  ---  ---    ---------------  -------------  -----------------------
 21 Container   memory    -    300Mi  200Mi            200Mi          -

8.5 創建應用

  1 [student@workstation ~]$ oc login -u developer -p redhat
  2 [student@workstation ~]$ oc new-app --name=load http://services.lab.example.com/node-hello
  3 [student@workstation ~]$ oc get pods
  4 NAME           READY     STATUS      RESTARTS   AGE
  5 load-1-6szhm   1/1       Running     0          6s
  6 load-1-build   0/1       Completed   0          43s
  7 [student@workstation ~]$ oc describe pod load-1-6szhm

8.6 擴大資源請求

  1 [student@workstation ~]$ oc set resources dc load --requests=memory=350M
  2 [student@workstation ~]$ oc get events | grep Warning

結論:請求資源超過limit限制,則會出現如上告警。

  1 [student@workstation ~]$ oc set resources dc load --requests=memory=200Mi

8.7 創建ResourceQuota

  1 [student@workstation ~]$ oc status ; oc get pod

  1 [student@workstation ~]$ oc login -u admin -p redhat
  2 [student@workstation ~]$ cat /home/student/DO280/labs/monitor-review/quotas.yml
  3 apiVersion: "v1"
  4 kind: "LimitRange"
  5 metadata:
  6   name: "review-limits"
  7 spec:
  8   limits:
  9     - type: "Container"
 10       max:
 11         memory: "300Mi"
 12       default:
 13         memory: "200Mi"
 14 [student@workstation ~]$ oc create -f /home/student/DO280/labs/monitor-review/quotas.yml
 15 [student@workstation ~]$ oc describe quota
 16 Name:            review-quotas
 17 Namespace:       load-review
 18 Resource         Used  Hard
 19 --------         ----  ----
 20 requests.memory  200M  600Mi   -

8.8 創建應用

  1 [student@workstation ~]$ oc login -u developer -p redhat
  2 [student@workstation ~]$ oc scale --replicas=4 dc load
  3 [student@workstation ~]$ oc get pods
  4 NAME           READY     STATUS      RESTARTS   AGE
  5 load-1-build   0/1       Completed   0          7m
  6 load-3-577fc   1/1       Running     0          5s
  7 load-3-nnncf   1/1       Running     0          4m
  8 load-3-nps4w   1/1       Running     0          5s
  9 [student@workstation ~]$  oc get events | grep Warning

結論:當前已應用配額規定會阻止創建第四個pod。

  1 [student@workstation ~]$ oc scale --replicas=1 dc load

8.9 暴露服務

  1 [student@workstation ~]$ oc expose svc load --hostname=load-review.apps.lab.example.com

8.10 創建探針

Web控制台創建。

Applications ——> Deployments ——> Actions ——> Edit Health Checks。

9.11 確認驗證

導航到Applications ——> Deployments,選擇應用程序的最新部署。

在Template部分中,找到以下條目:

  1 [student@workstation ~]$ lab review-monitor grade #腳本判斷結果
  2 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

01 . 容器編排簡介及Kubernetes核心概念

Kubernetes簡介

Kubernetes是谷歌嚴格保密十幾年的秘密武器—Borg的一個開源版本,是Docker分佈式系統解決方案.2014年由Google公司啟動.

Kubernetes提供了面嚮應用的容器集群部署和管理系統。Kubernetes的目標旨在消除編排物理/虛擬計算,網絡和存儲基礎設施的負擔,並使應用程序運營商和開發人員完全將重點放在以容器為中心的原語上進行自助運營。Kubernetes 也提供穩定、兼容的基礎(平台),用於構建定製化的workflows 和更高級的自動化任務。 Kubernetes 具備完善的集群管理能力,包括多層次的安全防護和准入機制、多租戶應用支撐能力、透明的服務註冊和服務發現機制、內建負載均衡器、故障發現和自我修復能力、服務滾動升級和在線擴容、可擴展的資源自動調度機制、多粒度的資源配額管理能力。 Kubernetes 還提供完善的管理工具,涵蓋開發、部署測試、運維監控等各個環節。

Kubernetes作為雲原生應用的基石,相當於一個雲操作系統,其重要性不言而喻。

容器編排

容器編排引擎三足鼎立:

    Mesos
    Docker Swarm+compose
    Kubernetes

早在 2015 年 5 月,Kubernetes 在 Google 上的搜索熱度就已經超過了 Mesos 和 Docker Swarm,從那兒之後更是一路飆升,將對手甩開了十幾條街,容器編排引擎領域的三足鼎立時代結束。

目前,AWS、Azure、Google、阿里雲、騰訊雲等主流公有雲提供的是基於 Kubernetes 的容器服務;Rancher、CoreOS、IBM、Mirantis、Oracle、Red Hat、VMWare 等無數廠商也在大力研發和推廣基於 Kubernetes 的容器 CaaS 或 PaaS 產品。可以說,Kubernetes 是當前容器行業最炙手可熱的明星。

Google 的數據中心裏運行着超過 20 億個容器,而且 Google 十年前就開始使用容器技術。

最初,Google 開發了一個叫 Borg 的系統(現在命名為 Omega)來調度如此龐大數量的容器和工作負載。在積累了這麼多年的經驗后,Google 決定重寫這個容器管理系統,並將其貢獻到開源社區,讓全世界都能受益。這個項目就是 Kubernetes。簡單的講,Kubernetes 是 Google Omega 的開源版本。

跟很多基礎設施領域先有工程實踐、後有方法論的發展路線不同,Kubernetes 項目的理論基礎則要比工程實踐走得靠前得多,這當然要歸功於 Google 公司在 2015 年 4 月發布的 Borg 論文了。

Borg 系統,一直以來都被譽為 Google 公司內部最強大的”秘密武器”。雖然略顯誇張,但這個說法倒不算是吹牛。

因為,相比於 Spanner、BigTable 等相對上層的項目,Borg 要承擔的責任,是承載 Google 公司整個基礎設施的核心依賴。在 Google 公司已經公開發表的基礎設施體系論文中,Borg 項目當仁不讓地位居整個基礎設施技術棧的最底層。

由於這樣的定位,Borg 可以說是 Google 最不可能開源的一個項目。而幸運地是,得益於 Docker 項目和容器技術的風靡,它卻終於得以以另一種方式與開源社區見面,這個方式就是 Kubernetes 項目。

所以,相比於”小打小鬧”的 Docker 公司、”舊瓶裝新酒”的 Mesos 社區,Kubernetes 項目從一開始就比較幸運地站上了一個他人難以企及的高度:在它的成長階段,這個項目每一個核心特性的提出,幾乎都脫胎於 Borg/Omega 系統的設計與經驗。更重要的是,這些特性在開源社區落地的過程中,又在整個社區的合力之下得到了極大的改進,修復了很多當年遺留在 Borg 體系中的缺陷和問題。

所以,儘管在發布之初被批評是”曲高和寡”,但是在逐漸覺察到 Docker 技術棧的”稚嫩”和 Mesos 社區的”老邁”之後,這個社區很快就明白了:k8s 項目在 Borg 體系的指導下,體現出了一種獨有的”先進性”與”完備性”,而這些特質才是一個基礎設施領域開源項目賴以生存的核心價值。

什麼是編排

一個正在運行的 Linux 容器,可以分成兩部分看待

1 . 容器的靜態視圖

一組聯合掛載在 /var/lib/docker/aufs/mnt 上的 rootfs,這一部分稱為”容器鏡像”(Container Image)

2 . 容器的動態視圖

一個由 Namespace+Cgroups 構成的隔離環境,這一部分稱為”容器運行時”(Container Runtime)

作為一名開發者,其實並不關心容器運行時的差異。在整個”開發 – 測試 – 發布”的流程中,真正承載着容器信息進行傳遞的,是容器鏡像,而不是容器運行時。

這正是容器技術圈在 Docker 項目成功后不久,就迅速走向了”容器編排”這個”上層建築”的主要原因:作為一家雲服務商或者基礎設施提供商,我只要能夠將用戶提交的 Docker 鏡像以容器的方式運行起來,就能成為這個非常熱鬧的容器生態圖上的一個承載點,從而將整個容器技術棧上的價值,沉澱在我的這個節點上。

更重要的是,只要從這個承載點向 Docker 鏡像製作者和使用者方向回溯,整條路徑上的各個服務節點,比如 CI/CD、監控、安全、網絡、存儲等等,都有可以發揮和盈利的餘地。這個邏輯,正是所有雲計算提供商如此熱衷於容器技術的重要原因:通過容器鏡像,它們可以和潛在用戶(即,開發者)直接關聯起來。

從一個開發者和單一的容器鏡像,到無數開發者和龐大的容器集群,容器技術實現了從”容器”到”容器雲”的飛躍,標志著它真正得到了市場和生態的認可。

這樣,容器就從一個開發者手裡的小工具,一躍成為了雲計算領域的絕對主角;而能夠定義容器組織和管理規範的”容器編排”技術,則當仁不讓地坐上了容器技術領域的”頭把交椅”。

最具代表性的容器編排工具

# 1. Docker 公司的 Compose+Swarm 組合
# 2. Google 與 RedHat 公司共同主導的 Kubernetes 項目

編排工具

Swarm與CoreOS

Docker 公司發布 Swarm 項目

Docker 公司在 2014 年發布 Swarm 項目. 一個有意思的事實:雖然通過”容器”這個概念完成了對經典 PaaS 項目的”降維打擊”,但是 Docker 項目和 Docker 公司,兜兜轉轉了一年多,卻還是回到了 PaaS 項目原本深耕多年的那個戰場:如何讓開發者把應用部署在我的項目上

Docker 項目從發布之初就全面發力,從技術、社區、商業、市場全方位爭取到的開發者群體,實際上是為此後吸引整個生態到自家”PaaS”上的一個鋪墊。只不過這時,”PaaS”的定義已經全然不是 Cloud Foundry 描述的那個樣子,而是變成了一套以 Docker 容器為技術核心,以 Docker 鏡像為打包標準的、全新的”容器化”思路。

這正是 Docker 項目從一開始悉心運作”容器化”理念和經營整個 Docker 生態的主要目的。

Docker 公司在 Docker 項目已經取得巨大成功后,執意要重新走回 PaaS 之路的原因:

雖然 Docker 項目備受追捧,但用戶們最終要部署的,還是他們的網站、服務、數據庫,甚至是雲計算業務。只有那些能夠為用戶提供平台層能力的工具,才會真正成為開發者們關心和願意付費的產品。而 Docker 項目這樣一個只能用來創建和啟停容器的小工具,最終只能充當這些平台項目的”幕後英雄”。

Docker 公司的老朋友和老對手 CoreOS:

CoreOS 是一個基礎設施領域創業公司。 核心產品是一個定製化的操作系統,用戶可以按照分佈式集群的方式,管理所有安裝了這個操作系統的節點。從而,用戶在集群里部署和管理應用就像使用單機一樣方便了。

Docker 項目發布后,CoreOS 公司很快就認識到可以把”容器”的概念無縫集成到自己的這套方案中,從而為用戶提供更高層次的 PaaS 能力。所以,CoreOS 很早就成了 Docker 項目的貢獻者,並在短時間內成為了 Docker 項目中第二重要的力量。

2014 年底,CoreOS 公司與 Docker 公司停止合作,並推出自己研製的 Rocket(後來叫 rkt)容器。

原因是 Docker 公司對 Docker 項目定位的不滿足。Docker 公司的解決方法是讓 Docker 項目提供更多的平台層能力,即向 PaaS 項目進化。這與 CoreOS 公司的核心產品和戰略發生了嚴重衝突。

Docker 公司在 2014 年就已經定好了平台化的發展方向,並且絕對不會跟 CoreOS 在平台層面開展任何合作。這樣看來,Docker 公司在 2014 年 12 月的 DockerCon 上發布 Swarm 的舉動,也就一點都不突然了。

CoreOS 項目

依託於一系列開源項目(比如 Container Linux 操作系統、Fleet 作業調度工具、systemd 進程管理和 rkt 容器),一層層搭建起來的平台產品

Swarm 項目:

以一個完整的整體來對外提供集群管理功能。Swarm 的最大亮點是它完全使用 Docker 項目原本的容器管理 API 來完成集群管理,比如:

單機 Docker 項目
docker run 我的容器

多機 Docker 項目

“docker run -H ” 我的 Swarm 集群 API 地址 ” ” 我的容器 “`

在部署了 Swarm 的多機環境下,用戶只需使用原先的 Docker 指令創建一個容器,這個請求就會被 Swarm 攔截下來處理,然後通過具體的調度算法找到一個合適的 Docker Daemon 運行起來。

這個操作方式簡潔明了,對於已經了解過 Docker 命令行的開發者們也很容易掌握。所以,這樣一個”原生”的 Docker 容器集群管理項目一經發布,就受到了已有 Docker 用戶群的熱捧。相比之下,CoreOS 的解決方案就顯得非常另類,更不用說用戶還要去接受完全讓人摸不着頭腦、新造的容器項目 rkt 了。

Swarm 項目只是 Docker 公司重新定義”PaaS”的關鍵一環。2014 年到 2015 年這段時間里,Docker 項目的迅速走紅催生出了一個非常繁榮的”Docker 生態”。在這個生態里,圍繞着 Docker 在各個層次進行集成和創新的項目層出不窮.

cncf(Fig/Compose)

Fig 項目

被docker收購后改名為 Compose

Fig 項目基本上只是靠兩個人全職開發和維護的,可它卻是當時 GitHub 上熱度堪比 Docker 項目的明星。

Fig 項目受歡迎的原因

是它在開發者面前第一次提出”容器編排”(Container Orchestration)的概念。

“編排”(Orchestration)在雲計算行業里不算是新詞彙,主要是指用戶如何通過某些工具或者配置來完成一組虛擬機以及關聯資源的定義、配置、創建、刪除等工作,然後由雲計算平台按照這些指定的邏輯來完成的過程。

容器時代,”編排”就是對 Docker 容器的一系列定義、配置和創建動作的管理。而 Fig 的工作實際上非常簡單:假如現在用戶需要部署的是應用容器 A、數據庫容器 B、負載均衡容器 C,那麼 Fig 就允許用戶把 A、B、C 三個容器定義在一個配置文件中,並且可以指定它們之間的關聯關係,比如容器 A 需要訪問數據庫容器 B。

接下來,只需執行一條非常簡單的指令:# fig up

Fig 就會把這些容器的定義和配置交給 Docker API 按照訪問邏輯依次創建,一系列容器就都啟動了;而容器 A 與 B 之間的關聯關係,也會交給 Docker 的 Link 功能通過寫入 hosts 文件的方式進行配置。更重要的是,你還可以在 Fig 的配置文件里定義各種容器的副本個數等編排參數,再加上 Swarm 的集群管理能力,一個活脫脫的 PaaS 呼之欲出。

它成了 Docker 公司到目前為止第二大受歡迎的項目,一直到今也依然被很多人使用。

當時的這個容器生態里,還有很多開源項目或公司。比如:

專門負責處理容器網絡的 SocketPlane 項目(後來被 Docker 公司收購)

專門負責處理容器存儲的 Flocker 項目(後來被 EMC 公司收購)

專門給 Docker 集群做圖形化管理界面和對外提供雲服務的 Tutum 項目(後來被 Docker 公司收購)等等。

Mesosphere與Mesos

老牌集群管理項目 Mesos 和它背後的創業公司 Mesosphere:Mesos 社區獨特的競爭力:

超大規模集群的管理經驗

Mesos 早已通過了萬台節點的驗證,2014 年之後又被廣泛使用在 eBay 等大型互聯網公司的生產環境中。

Mesos 是 Berkeley 主導的大數據套件之一,是大數據火熱時最受歡迎的資源管理項目,也是跟 Yarn 項目殺得難捨難分的實力派选手。

大數據所關注的計算密集型離線業務,其實並不像常規的 Web 服務那樣適合用容器進行託管和擴容,也沒有對應用打包的強烈需求,所以 Hadoop、Spark 等項目到現在也沒在容器技術上投下更大的賭注;

但對於 Mesos 來說,天生的兩層調度機制讓它非常容易從大數據領域抽身,轉而去支持受眾更加廣泛的 PaaS 業務。

在這種思路指導下,Mesosphere 公司發布了一個名為 Marathon 的項目,這個項目很快就成為 Docker Swarm 的一個有力競爭對手。

通過 Marathon 實現了諸如應用託管和負載均衡的 PaaS 功能之後,Mesos+Marathon 的組合實際上進化成了一個高度成熟的 PaaS 項目,同時還能很好地支持大數據業務。

Mesosphere 公司提出”DC/OS”(數據中心操作系統)的口號和產品:

旨在使用戶能夠像管理一台機器那樣管理一個萬級別的物理機集群,並且使用 Docker 容器在這個集群里自由地部署應用。這對很多大型企業來說具有着非同尋常的吸引力。

這時的容器技術生態, CoreOS 的 rkt 容器完全打不開局面,Fleet 集群管理項目更是少有人問津,CoreOS 完全被 Docker 公司壓制了。

RedHat 也是因為對 Docker 公司平台化戰略不滿而憤憤退出。但此時,它竟只剩下 OpenShift 這個跟 Cloud Foundry 同時代的經典 PaaS 一張牌可以打,跟 Docker Swarm 和轉型后的 Mesos 完全不在同一個”競技水平”之上。

google與k8s

2014 年 6 月,基礎設施領域的翹楚 Google 公司突然發力,正宣告了一個名叫 Kubernetes 項目的誕生。這個項目,不僅挽救了當時的 CoreOS 和 RedHat,還如同當年 Docker 項目的橫空出世一樣,再一次改變了整個容器市場的格局。

這段時間,也正是 Docker 生態創業公司們的春天,大量圍繞着 Docker 項目的網絡、存儲、監控、CI/CD,甚至 UI 項目紛紛出台,也湧現出了很多 Rancher、Tutum 這樣在開源與商業上均取得了巨大成功的創業公司。

在 2014~2015 年間,整個容器社區可謂熱鬧非凡。

這令人興奮的繁榮背後,卻浮現出了更多的擔憂。這其中最主要的負面情緒,是對 Docker 公司商業化戰略的種種顧慮。

事實上,很多從業者也都看得明白,Docker 項目此時已經成為 Docker 公司一個商業產品。而開源,只是 Docker 公司吸引開發者群體的一個重要手段。不過這麼多年來,開源社區的商業化其實都是類似的思路,無非是高不高調、心不心急的問題罷了。

而真正令大多數人不滿意的是,Docker 公司在 Docker 開源項目的發展上,始終保持着絕對的權威和發言權,並在多個場合用實際行動挑戰到了其他玩家(比如,CoreOS、RedHat,甚至谷歌和微軟)的切身利益。

那麼,這個時候,大家的不滿也就不再是在 GitHub 上發發牢騷這麼簡單了。

相信很多容器領域的老玩家們都聽說過,Docker 項目剛剛興起時,Google 也開源了一個在內部使用多年、經歷過生產環境驗證的 Linux 容器:lmctfy(Let Me Container That For You)。

然而,面對 Docker 項目的強勢崛起,這個對用戶沒那麼友好的 Google 容器項目根本沒有招架之力。所以,知難而退的 Google 公司,向 Docker 公司表示了合作的願望:關停這個項目,和 Docker 公司共同推進一个中立的容器運行時(container runtime)庫作為 Docker 項目的核心依賴。

不過,Docker 公司並沒有認同這個明顯會削弱自己地位的提議,還在不久后,自己發布了一個容器運行時庫 Libcontainer。這次匆忙的、由一家主導的、並帶有戰略性考量的重構,成了 Libcontainer 被社區長期詬病代碼可讀性差、可維護性不強的一個重要原因。

至此,Docker 公司在容器運行時層面上的強硬態度,以及 Docker 項目在高速迭代中表現出來的不穩定和頻繁變更的問題,開始讓社區叫苦不迭。

這種情緒在 2015 年達到了一個高潮,容器領域的其他幾位玩家開始商議”切割”Docker 項目的話語權。而”切割”的手段也非常經典,那就是成立一个中立的基金會。

於是,2015 年 6 月 22 日,由 Docker 公司牽頭,CoreOS、Google、RedHat 等公司共同宣布,Docker 公司將 Libcontainer 捐出,並改名為 RunC 項目,交由一個完全中立的基金會管理,然後以 RunC 為依據,大家共同制定一套容器和鏡像的標準和規範。

這套標準和規範,就是 OCI( Open Container Initiative )。OCI 的提出,意在將容器運行時和鏡像的實現從 Docker 項目中完全剝離出來。這樣做,一方面可以改善 Docker 公司在容器技術上一家獨大的現狀,另一方面也為其他玩家不依賴於 Docker 項目構建各自的平台層能力提供了可能。

不過,OCI 的成立更多的是這些容器玩家出於自身利益進行干涉的一個妥協結果。儘管 Docker 是 OCI 的發起者和創始成員,它卻很少在 OCI 的技術推進和標準制定等事務上扮演關鍵角色,也沒有動力去积極地推進這些所謂的標準。

這也是迄今為止 OCI 組織效率持續低下的根本原因。

OCI 並沒能改變 Docker 公司在容器領域一家獨大的現狀,Google 和 RedHat 等公司於是把第二把武器擺上了檯面。

Docker 之所以不擔心 OCI 的威脅,原因就在於它的 Docker 項目是容器生態的事實標準,而它所維護的 Docker 社區也足夠龐大。可是,一旦這場鬥爭被轉移到容器之上的平台層,或者說 PaaS 層,Docker 公司的競爭優勢便立刻捉襟見肘了。

在這個領域里,像 Google 和 RedHat 這樣的成熟公司,都擁有着深厚的技術積累;而像 CoreOS 這樣的創業公司,也擁有像 Etcd 這樣被廣泛使用的開源基礎設施項目。

可是 Docker 公司卻只有一個 Swarm。

所以這次,Google、RedHat 等開源基礎設施領域玩家們,共同牽頭髮起了一個名為 CNCF(Cloud Native Computing Foundation)的基金會。這個基金會的目的其實很容易理解:它希望,以 Kubernetes 項目為基礎,建立一個由開源基礎設施領域廠商主導的、按照獨立基金會方式運營的平台級社區,來對抗以 Docker 公司為核心的容器商業生態。

為了打造出一個圍繞 Kubernetes 項目的”護城河”,CNCF 社區就需要至少確保兩件事情:

# 1. Kubernetes 項目必須能夠在容器編排領域取得足夠大的競爭優勢
# 2. CNCF 社區必須以 Kubernetes 項目為核心,覆蓋足夠多的場景

CNCF 社區如何解決 Kubernetes 項目在編排領域的競爭力的問題:

在容器編排領域,Kubernetes 項目需要面對來自 Docker 公司和 Mesos 社區兩個方向的壓力。Swarm 和 Mesos 實際上分別從兩個不同的方向講出了自己最擅長的故事:Swarm 擅長的是跟 Docker 生態的無縫集成,而 Mesos 擅長的則是大規模集群的調度與管理。

這兩個方向,也是大多數人做容器集群管理項目時最容易想到的兩個出發點。也正因為如此,Kubernetes 項目如果繼續在這兩個方向上做文章恐怕就不太明智了。

Kubernetes 選擇的應對方式是:Borg

k8s 項目大多來自於 Borg 和 Omega 系統的內部特性,這些特性落到 k8s 項目上,就是 Pod、Sidecar 等功能和設計模式。

這就解釋了,為什麼 Kubernetes 發布后,很多人”抱怨”其設計思想過於”超前”的原因:Kubernetes 項目的基礎特性,並不是幾個工程師突然”拍腦袋”想出來的東西,而是 Google 公司在容器化基礎設施領域多年來實踐經驗的沉澱與升華。這正是 Kubernetes 項目能夠從一開始就避免同 Swarm 和 Mesos 社區同質化的重要手段。

CNCF 接下來的任務是如何把這些先進的思想通過技術手段在開源社區落地,並培育出一個認同這些理念的生態?

RedHat 發揮了重要作用。當時,Kubernetes 團隊規模很小,能夠投入的工程能力十分緊張,這恰恰是 RedHat 的長處。RedHat 更是世界上為數不多、能真正理解開源社區運作和項目研發真諦的合作夥伴。

RedHat 與 Google 聯盟的成立,不僅保證了 RedHat 在 Kubernetes 項目上的影響力,也正式開啟了容器編排領域”三國鼎立”的局面。

Mesos 社區與容器技術的關係,更像是”借勢”,而不是這個領域真正的參与者和領導者。這個事實,加上它所屬的 Apache 社區固有的封閉性,導致了 Mesos 社區雖然技術最為成熟,卻在容器編排領域鮮有創新。

一開始,Docker 公司就把應對 Kubernetes 項目的競爭擺在首要位置:
一方面,不斷強調”Docker Native”的”重要性”
一方面,與 k8s 項目在多個場合進行了直接的碰撞。

這次競爭的發展態勢,很快就超過了 Docker 公司的預期。

Kubernetes 項目並沒有跟 Swarm 項目展開同質化的競爭
所以 “Docker Native”的說辭並沒有太大的殺傷力
相反 k8s 項目讓人耳目一新的設計理念和號召力,很快就構建出了一個與眾不同的容器編排與管理的生態。

Kubernetes 項目在 GitHub 上的各項指標開始一騎絕塵,將 Swarm 項目遠遠地甩在了身後.

CNCF 社區如何解決第二個問題:

在已經囊括了容器監控事實標準的 Prometheus 項目后,CNCF 社區迅速在成員項目中添加了 Fluentd、OpenTracing、CNI 等一系列容器生態的知名工具和項目。

而在看到了 CNCF 社區對用戶表現出來的巨大吸引力之後,大量的公司和創業團隊也開始專門針對 CNCF 社區而非 Docker 公司制定推廣策略。

2016 年,Docker 公司宣布了一個震驚所有人的計劃:放棄現有的 Swarm 項目,將容器編排和集群管理功能全部內置到 Docker 項目當中。

Docker 公司意識到了 Swarm 項目目前唯一的競爭優勢,就是跟 Docker 項目的無縫集成。那麼,如何讓這種優勢最大化呢?那就是把 Swarm 內置到 Docker 項目當中。

從工程角度來看,這種做法的風險很大。內置容器編排、集群管理和負載均衡能力,固然可以使得 Docker 項目的邊界直接擴大到一個完整的 PaaS 項目的範疇,但這種變更帶來的技術複雜度和維護難度,長遠來看對 Docker 項目是不利的。

不過,在當時的大環境下,Docker 公司的選擇恐怕也帶有一絲孤注一擲的意味。

k8s 的應對策略

是反其道而行之,開始在整個社區推進”民主化”架構,即:從 API 到容器運行時的每一層,Kubernetes 項目都為開發者暴露出了可以擴展的插件機制,鼓勵用戶通過代碼的方式介入到 Kubernetes 項目的每一個階段。

Kubernetes 項目的這個變革的效果立竿見影,很快在整個容器社區中催生出了大量的、基於 Kubernetes API 和擴展接口的二次創新工作,比如:
目前熱度極高的微服務治理項目 Istio;
被廣泛採用的有狀態應用部署框架 Operator;
還有像 Rook 這樣的開源創業項目,它通過 Kubernetes 的可擴展接口,把 Ceph 這樣的重量級產品封裝成了簡單易用的容器存儲插件。

在鼓勵二次創新的整體氛圍當中,k8s 社區在 2016 年後得到了空前的發展。更重要的是,不同於之前局限於”打包、發布”這樣的 PaaS 化路線,這一次容器社區的繁榮,是一次完全以 Kubernetes 項目為核心的”百花爭鳴”。

面對 Kubernetes 社區的崛起和壯大,Docker 公司也不得不面對自己豪賭失敗的現實。但在早前拒絕了微軟的天價收購之後,Docker 公司實際上已經沒有什麼迴旋餘地,只能選擇逐步放棄開源社區而專註於自己的商業化轉型。

所以,從 2017 年開始,Docker 公司先是將 Docker 項目的容器運行時部分 Containerd 捐贈給 CNCF 社區,標志著 Docker 項目已經全面升級成為一個 PaaS 平台;緊接着,Docker 公司宣布將 Docker 項目改名為 Moby,然後交給社區自行維護,而 Docker 公司的商業產品將佔有 Docker 這個註冊商標。

Docker 公司這些舉措背後的含義非常明確:它將全面放棄在開源社區同 Kubernetes 生態的競爭,轉而專註於自己的商業業務,並且通過將 Docker 項目改名為 Moby 的舉動,將原本屬於 Docker 社區的用戶轉化成了自己的客戶。

2017 年 10 月,Docker 公司出人意料地宣布,將在自己的主打產品 Docker 企業版中內置 Kubernetes 項目,這標志著持續了近兩年之久的”編排之爭”至此落下帷幕。

2018 年 1 月 30 日,RedHat 宣布斥資 2.5 億美元收購 CoreOS。

2018 年 3 月 28 日,這一切紛爭的始作俑者,Docker 公司的 CTO Solomon Hykes 宣布辭職,曾經紛紛擾擾的容器技術圈子,到此塵埃落定。

容器技術圈子在短短几年裡發生了很多變數,但很多事情其實也都在情理之中。就像 Docker 這樣一家創業公司,在通過開源社區的運作取得了巨大的成功之後,就不得不面對來自整個雲計算產業的競爭和圍剿。而這個產業的垄斷特性,對於 Docker 這樣的技術型創業公司其實天生就不友好。

在這種局勢下,接受微軟的天價收購,在大多數人看來都是一個非常明智和實際的選擇。可是 Solomon Hykes 卻多少帶有一些理想主義的影子,既然不甘於”寄人籬下”,那他就必須帶領 Docker 公司去對抗來自整個雲計算產業的壓力。

只不過,Docker 公司最後選擇的對抗方式,是將開源項目與商業產品緊密綁定,打造了一個極端封閉的技術生態。而這,其實違背了 Docker 項目與開發者保持親密關係的初衷。相比之下,Kubernetes 社區,正是以一種更加溫和的方式,承接了 Docker 項目的未盡事業,即:以開發者為核心,構建一個相對民主和開放的容器生態。

這也是為何,Kubernetes 項目的成功其實是必然的。

很難想象如果 Docker 公司最初選擇了跟 Kubernetes 社區合作,如今的容器生態又將會是怎樣的一番景象。不過我們可以肯定的是,Docker 公司在過去五年裡的風雲變幻,以及 Solomon Hykes 本人的傳奇經歷,都已經在雲計算的長河中留下了濃墨重彩的一筆。

小結
# 1. 容器技術的興起源於 PaaS 技術的普及;
# 2. Docker 公司發布的 Docker 項目具有里程碑式的意義;
# 3. Docker 項目通過"容器鏡像",解決了應用打包這個根本性難題。

# 容器本身沒有價值,有價值的是"容器編排"。
# 也正因為如此,容器技術生態才爆發了一場關於"容器編排"的"戰爭"。而這次戰爭,最終以 Kubernetes 項目和 CNCF 社區的勝利而告終。

Kubernetes核心概念

什麼是Kubernetes?

Kubernetes是一個完備的分佈式系統支撐平台。

Kubernetes具有完備的集群管理能力,包括多層次的安全防護和准入機制/多租戶應用支撐能力、透明的服務註冊和服務發現機制、內建智能負載均衡器、強大的故障發現和自我修復功能、服務滾動升級和在線擴容能力、可擴展的資源自動調度機制,以及多粒度的資源配額管理能力。同時kubernetes提供了完善的管理工具,這些工具覆蓋了包括開發、測試部署、運維監控在內的各個環節;因此kubernetes是一個全新的基於容器技術的分佈式架構解決方案,並且是一個一站式的完備的分佈式系統開發和支撐平台.

Kubernetes Service介紹

Service是分佈式集群結構的核心,一個Server對象有以下關鍵特徵:

# 1. 擁有一個唯一指定的名字(比如mysql-server)
# 2. 擁有一個虛擬IP(Cluster IP,Service IP或VIP和端口號)
# 3. 能夠提供某種遠程服務能力
# 4. 被映射到了提供這種服務能力的一組容器應用上.

Service的服務進程目前都基於Socker通信方式對外提供服務,比如redis、memcache、MySQL、Web Server,或者是實現了某個具體業務的一個特定的TCP Server進程。雖然一個Service通常由多個相關的服務進程來提供服務,每個服務進程都有一個獨立的Endpoint(IP+Port)訪問點,但Kubernetes 能夠讓我們通過Service虛擬Cluster IP+Service Port連接到指定的Service上。有了Kubernetes內建的透明負載均衡和故障恢復機制,不管後端有多少服務進程,也不管某個服務進程是否會由於發生故障而重新部署到其他機器,都不會影響到我們對服務的正常調用。更重要的是這個Service本身一旦創建就不再變化,這意味着Kubernetes集群中,我們再也不用為了服務的IP地址變來變去的問題而頭疼。

Kubernetes Pod介紹

Pod概念 Pod運行在一個我們稱之為Node的環境中,可以是私有雲也可以是公有雲的虛擬機或者物理機上,通常在一個節點上運行幾百個Pod,每個Pod運行着一個特殊的稱之為Pause的容器,其他容器則為業務容器,這些業務容器共享着Pause容器的網絡棧和Volume掛載卷,因此他們之間的通訊和數據交換更為高效,在設計時我們充分利用這一特徵將一組密切相關的服務進程放入同一個Pod中.

並不是每個Pod和它裏面的容器都映射到一個Service上,只是那些提供服務(無論是內還是對外)的一組Pod才會被映射成一個服務.

Service和Pod如何關聯

容器提供了強大的隔離功能,所以有必要把Service提供服務的這組容器放入到容器中隔離,Kubernetes設計了Pod服務,將每個服務進程包裝成相應的Pod中,使其成為Pod中運行的一個容器Container,為了建立Service和Pod間的關聯關係,Kubernetes首先給每個Pod貼上了一個標籤Label,給運行Mysql的Pod貼上了name=mysql標籤,給運行PHP貼上name=php標籤,然後給相應的Service定義標籤選擇器Label Selector,比如Mysql Service的標籤選擇器選擇條件為name=mysql,意為該Service要作用於所有包含name=mysql Label的Pod上,這樣就巧妙的解決了Service和Pod關聯的問題.

Kubernetes RC介紹

RC介紹在Kubernetes集群中,你只需要為需要擴容的Service關聯的Pod創建一個RC(Replication Controller),則該Service的擴容以至於後來的Service升級等頭疼問題都可以迎刃而解,定義一個RC文件包含以下3個關鍵點.

# 1. 目標Pod的定義
# 2. 目標Pod需要運行的副本數量(Replicas)
# 3. 要監控的目標Pod的標籤(Label)

在創建好RC系統自動創建號Pod后,Kubernetes會通過RC中定義的Label篩選出對應的Pod實例並監控其狀態和數量,如果實例數量少於定義的副本數量Replicas則會用RC中定義的Pod模板來創建一個新的Pod,然後將Pod調度到合適的Node上運行,直到Pod實例的數量達到預定目標,這個過程完全是自動化的,無需人干預,只需要修改RC中的副本數量即可.

Kubernetes Master介紹

Kubernetes 里的Master指的是集群控制節點,每個Kubernetes集群里需要有一個Master節點來負責整個集群的管理和控制,基本上Kubernetes所有的控制命令都發給它,它負責具體的執行過程,我們後面執行的所有命令基本上都是在Master節點上運行的。如果Master宕機或不可用,那麼集群內容器的管理都將失效.

Master節點上運行一下一組關鍵進程:

  1. Kubernetes API Server: 提供了HTTP Rest接口的關鍵服務進程,是Kubernetes里所有資源的增刪改查等操作的唯一入口,也是集群控制的入門進程.
  2. Kubernetes Controller Manager 里所有的資源對象的自動化控制中心.
  3. Kubernetes Scheduler: 負責資源調度(Pod調度)的進程

另外在Master節點還需要啟動一個etcd服務,因為Kubernetes里所有資源對象的數據全部保存在etcd中.

Kubernetes Node介紹

除了Master,集群中其他機器稱為Node節點,每個Node都會被分配一些工作負載Docker容器,當某個Node宕機,其上的工作負載都會被Master自動轉移到其他節點上去.

每個Node節點上都運行着以下一組關鍵進程

# 1. kubelet: 負責Pod對應的創建、停止等服務,同時與Master節點密切協作,實現集群管理的基本功能.
# 2. kube-proxy: 實現Kubernetes Service的通信與負載均衡機制的重要組件.
# 3. Docker Engine: Docker引擎,負責本機的容器創建和管理工作

在集群管理方面,Kubernetes將集群中的機器劃分為一個Master節點和一群工作節點(Node)中,在Master節點上運行着集群管理相關的一組進程kube-apiserver,kube-controller-manager和kube-scheduler,這些進程實現了整個集群的資源管理,Pod調度,彈性伸縮,安全控制,系統監控和糾錯等管理功能,並且都是全自動完成的、Node作為集群中的工作節點,運行真正的應用程序,在Node上Kubernetes最小運行單元是Pod,Node上運行着Kubernetes的Kubelet、kube-proxy服務進程,這些服務進程負責Pod創建、啟動、監控、重啟、銷毀以及軟件模式的負載均衡.

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

深入理解JVM(③)虛擬機性能監控、故障處理工具

前言

JDK的bin目錄中有一系列的小工具,除了java.exe、javac.exe這兩個編譯和運行Java程序外,還有打包、部署、簽名、調試、監控、運維等各種場景都會用到這些小工具。

這些工具根據軟件可用性和授權的不同,可以把它們劃分為三類:

  • 商業授權工具: 主要是JMC(Java Mission Control)及它要使用到的JFR(Java Flight Recorder),JMC在個人開發環境中使用是免費的,但是在商業環境中使用它則是付費的。
  • 正式支持工具: 這一類工具屬於被長期支持的工具,不同平台、不同版本的JDK之間,這類工具可能會略有差異,但是不會出現某一個工具突然消失的情況。
  • 實驗性工具: 這一類工具在它們的使用說明中被聲明為“沒有技術支持,並且是實驗性質的”(Unsupported and Experimental)產品,日後可能會轉載,也可能會在某個JDK版本中國無聲無息地消失。

jps:虛擬機進程狀態工具

JDK的一些小工具都參考了UNIX的命名方式,jps(JVM Process Status Tool)是其中的典型。
功能也是和UNIX的ps的命令類似:
可以列出正在運行的虛擬機進程,並显示虛擬機執行主類(Main Class,main()函數所在的類)名稱以及這些進程的本地虛擬機唯一ID(LVMID,Local Virtual Machine Identifier)。
jps命令格式:

jps [ options ]  [ hostid ]

jps工具主要選項:

jstat:虛擬機統計信息監視工具

jstat( JVM Statistics Monitoring Tool )是用戶監視虛擬機各種運行狀態信息的命令行工具。可以显示本地虛擬機進程中 類加載、內存、垃圾收集、即時編譯等運行時數據,這個命令是在服務器是哪個運行期定位虛擬機性能問題的常用工具。
jstat 命令格式為:

jstat [ option  vmid [ interval [ s | ms ] [ count ] ] ]

參數interval 和 count 代表查詢間隔和次數,如果省略這2個參數,說明只查詢一次假設需要每250毫秒查詢一次進程 1440 垃圾收集狀況,一共查詢20次,那命令應當是:

jstat -gc 1440 250 20 

option 代表用戶希望查詢的虛擬機信息,主要分三類:
類加載、垃圾收集、運行期間編譯狀況。
jstat工具主要選項

jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是實時查看和調整虛擬機各項參數。使用jps命令的-v參數可以查看虛擬機啟動時显示指定的參數列表,但如果想知道未被显示指定的參數的系統默認值,除了去找資料外,就只能使用jinfo的-flag選項進行查詢了。jinfo還可以使用-sysprops選項把虛擬機進程的

System.getProperties()

的內容打出來。
jinfo 命令格式:

jinfo [ option ] pid

jmap:Java內存映像工具

jmap (Memory Map for Java)命令用於生成堆轉儲快照(一般稱為heapdump 或 dump文件)。
jmap的作用並不僅僅是為了獲取堆轉儲快照,它還可以查詢finalize執行隊列、Java堆和方法區的詳細信息,如空間使用率、當前用的是哪種收集器等。
jmap 命令格式:

jmap [ option ] vmid

jmap工具主要選項

jhat:虛擬機堆轉儲快照分析工具

JDK提供jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。jhat內置了一個微型的HTTP/Web服務器,生成堆轉儲快照的分析結果后,可以在瀏覽器中查看。但是一般在實際工作中,都不會直接使用jhat命令來分析堆轉儲快照文件,一是因為分析工作耗時而且極為耗費資源,一般不會直接在服務器上使用,而是在其他機器上進行分析。二是jhat的分析功能比較簡陋,不如VisualVM,以及一些專業的分析工具例如:Eclipse Memory Analyzer、IBM HeapAnalyzer。

jstack:Java堆棧跟蹤工具

jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照(一般稱為threaddump或者javacore文件)。
線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的目的通常是定位線程出現長時間停頓的原因,如線程死鎖、死循環、請求外部資源導致長時間掛起等,都是導致線程長時間停頓的常見原因。
jstack命令格式:

jstack [ option ] vmid 

線程出現停頓時通過jstack來查看各個線程的調用堆棧,就可以獲知沒有響應的線程到底在後頭做些什麼事情,或者等待着什麼資源。
jstack工具主要選項

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

野花如何用多樣性抗氣候變遷 基因研究說分明

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Newtonsoft 六個超簡單又實用的特性,值得一試 【下篇】

一:講故事

上一篇介紹的 6 個特性從園子里的反饋來看效果不錯,那這一篇就再帶來 6 個特性同大家一起欣賞。

二:特性分析

1. 像弱類型語言一樣解析 json

大家都知道弱類型的語言有很多,如: nodejs,python,php,它們有一個的地方就是處理json,不需要像 強類型語言 那樣還要給它配一個類,什麼意思呢? 就拿下面的 json 說事。


{
  "DisplayName": "新一代算法模型",
  "CustomerType": 1,
  "Report": {
    "TotalCustomerCount": 1000,
    "TotalTradeCount": 50
  },
  "CustomerIDHash": [1,2,3,4,5]
}

這個 json 如果想灌到 C# 中處理,你就得給它定義一個適配的類,就如 初篇 的客戶算法模型類,所以這裏就有了一個需求,能不能不定義類也可以自由解析上面這串 json 呢??? 哈哈,當然是可以的, 反序列化成 Dictionary 即可,就拿提取 Report.TotalCustomerCountCustomerIDHash 這兩個字段演示一下。


        static void Main(string[] args)
        {
            var json = @"{
                           'DisplayName': '新一代算法模型',
                           'CustomerType': 1,
                           'Report': {
                             'TotalCustomerCount': 1000,
                             'TotalTradeCount': 50
                           },
                           'CustomerIDHash': [1,2,3,4,5]
                         }";

            var dict = JsonConvert.DeserializeObject<Dictionary<object, object>>(json);

            var report = dict["Report"] as JObject;
            var totalCustomerCount = report["TotalCustomerCount"];

            Console.WriteLine($"totalCustomerCount={totalCustomerCount}");

            var arr = dict["CustomerIDHash"] as JArray;
            var list = arr.Select(m => m.Value<int>()).ToList();

            Console.WriteLine($"list={string.Join(",", list)}");
        }

2. 如何讓json中的枚舉保持更易讀的字符串型

這句話是什麼意思呢? 默認情況下, SerializeObject 會將 Model 中的 Enum 變成數值型,大家都知道數值型語義性是非常差的,如下代碼所示:


    static void Main(string[] args)
    {
        var model = new ThreadModel() { ThreadStateEnum = System.Threading.ThreadState.Running };

        var json = JsonConvert.SerializeObject(model);

        Console.WriteLine(json);
    }

    class ThreadModel
    {
        public System.Threading.ThreadState ThreadStateEnum { get; set; }
    }

對吧,確實語義特別差,那能不能直接生成 Running 這種字符串形式呢? 當然可以了。。。改造如下:


  var json = JsonConvert.SerializeObject(model, new StringEnumConverter());

這裏可能就有人鑽牛角尖了,能不能部分指定讓枚舉生成 string,其他的生成 int ,沒關係,這也難不倒我,哪裡使用就用 JsonConverter 標記哪裡。。。


        static void Main(string[] args)
        {
            var model = new ThreadModel()
            {
                ThreadStateEnum = System.Threading.ThreadState.Running,
                TaskStatusEnum = TaskStatus.RanToCompletion
            };

            var json = JsonConvert.SerializeObject(model);

            Console.WriteLine(json);
        }

        class ThreadModel
        {
            public System.Threading.ThreadState ThreadStateEnum { get; set; }

            [JsonConverter(typeof(StringEnumConverter))]
            public TaskStatus TaskStatusEnum { get; set; }
        }        

3. 格式化 json 中的時間類型

在 model 轉化成 json 的過程中,總少不了 時間類型,為了讓時間類型 可讀性更高,通常會 格式化為 YYYY年/MM月/dd日 ,那如何實現呢? 很簡單撒,在 JsonConvert 中也是一個 枚舉 幫你搞定。。。


        static void Main(string[] args)
        {
            var json = JsonConvert.SerializeObject(new Order()
            {
                OrderTitle = "女裝大佬",
                Created = DateTime.Now
            }, new JsonSerializerSettings
            {
                DateFormatString = "yyyy年/MM月/dd日",
            });

            Console.WriteLine(json);
        }
        public class Order
        {
            public string OrderTitle { get; set; }
            public DateTime Created { get; set; }
        }   

對了,我記得很早的時候,C# 自帶了一個 JavaScriptSerializer, 也是用來進行 model 轉 json的,但是它會將 datetime 轉成 時間戳,而不是時間字符串形式,如果你因為特殊原因想通過 JsonConvert 將時間生成時間戳的話,也是可以的, 用 DateFormatHandling.MicrosoftDateFormat 枚舉指定一下即可,如下:

4. 對一些常用設置進行全局化

在之前所有演示的特性技巧中都是在 JsonConvert 上指定的,也就是說 100 個 JsonConvert 我就要指定 100 次,那有沒有類似一次指定,整個進程通用呢? 這麼強大的 Newtonsoft 早就支持啦, 就拿上面的 Order 舉例:


        JsonConvert.DefaultSettings = () =>
        {
            var settings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented
            };
            return settings;
        };

        var order = new Order() { OrderTitle = "女裝大佬", Created = DateTime.Now };

        var json1 = JsonConvert.SerializeObject(order);
        var json2 = JsonConvert.SerializeObject(order);

        Console.WriteLine(json1);
        Console.WriteLine(json2);

可以看到,Formatting.Indented 對兩串 json 都生效了。

5. 如何保證 json 到 model 的嚴謹性 及提取 json 未知字段

有時候我們有這樣的需求,一旦 json 中出現 model 未知的字段,有兩種選擇: 要麼報錯,要麼提取出未知字段,在 Newtonsoft 中默認的情況是忽略,場景大家可以自己找哈。

  • 未知字段報錯

        static void Main(string[] args)
        {
            var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

            var order = JsonConvert.DeserializeObject<Order>(json, new JsonSerializerSettings
            {
                MissingMemberHandling = MissingMemberHandling.Error
            });

            Console.WriteLine(order);
        }

        public class Order
        {
            public string OrderTitle { get; set; }
            public DateTime Created { get; set; }
            public override string ToString()
            {
                return $"OrderTitle={OrderTitle}, Created={Created}";
            }
        }        

  • 提取未知字段

我依稀的記得 WCF 在這種場景下也是使用一個 ExtenstionDataObject 來存儲客戶端傳過來的未知字段,有可能是客戶端的 model 已更新,server端還是舊版本,通常在 json 序列化中也會遇到這種情況,這裏只要使用 JsonExtensionData 特性就可以幫你搞定,在 OnDeserialized 這種AOP方法中進行攔截,如下代碼:


    static void Main(string[] args)
    {
        var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

        var order = JsonConvert.DeserializeObject<Order>(json);

        Console.WriteLine(order);
    }

    public class Order
    {
        public string OrderTitle { get; set; }

        public DateTime Created { get; set; }

        [JsonExtensionData]
        private IDictionary<string, JToken> _additionalData;

        public Order()
        {
            _additionalData = new Dictionary<string, JToken>();
        }

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            var dict = _additionalData;
        }

        public override string ToString()
        {
            return $"OrderTitle={OrderTitle}, Created={Created}";
        }
    }        

6. 開啟 JsonConvert 詳細日誌功能

有時候在查閱源碼的時候開啟日誌功能更加有利於理解源碼的內部運作,所以這也是一個非常實用的功能,看看如何配置吧。


        static void Main(string[] args)
        {
            var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

            MemoryTraceWriter traceWriter = new MemoryTraceWriter();

            var account = JsonConvert.DeserializeObject<Order>(json, new JsonSerializerSettings
            {
                TraceWriter = traceWriter
            });

            Console.WriteLine(traceWriter.ToString());
        }

        public class Order
        {
            public string OrderTitle { get; set; }

            public DateTime Created { get; set; }

            public override string ToString()
            {
                return $"OrderTitle={OrderTitle}, Created={Created}";
            }
        }

三:總結

嘿嘿,這篇 6 個特性就算說完了, 結合上一篇一共 12 個特性,是不是非常簡單且實用,後面準備給大家帶來一些源碼解讀吧! 希望本篇對您有幫助,謝謝!

如您有更多問題與我互動,掃描下方進來吧~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

CPU明明8個核,網卡為啥拚命折騰一號核?

中斷機制

我是CPU一號車間的阿Q,我又來了!

我們日常的工作就是不斷執行代碼指令,不過這看似簡單的工作背後其實也並不輕鬆。

咱不能悶着頭啥也不管一個勁的只管執行代碼,還得和連接在主板上的其他單位打交道。經常保持聯繫的有鍵盤、鼠標、磁盤,哦對,還有網卡,這傢伙最近把我惹到了,待會再說這事兒。

原以為內存那傢伙已經夠慢的了,沒想到跟上面這幾位通個信比他更慢,咱CPU工廠的時間一刻值千金,不能幹等着,耽誤工夫。後來廠里一合計,想了個叫中斷的辦法。

在我們車間裝了個大燈,這些單位想聯繫我們辦事兒,就先給我們發一个中斷信號,大燈就會自動亮起。我們平時工作執行代碼指令的時候,每執行一條指令就會瞅一眼看看大燈有沒有亮起來。一旦發現燈亮了,就把手頭的工作先放一邊,去處理一下。

我們記性很差的,等會處理了完了還得回來接着原來的活繼續干,為了等會回來還能接的起來,走之前得把當前執行的這個線程的各個寄存器的值,執行到哪裡了等等這些信息都保存在這個線程的棧里去。

不過有時候我們在執行非常重要的事情的時候,就不想被他們打斷。於是我們又在車間里那個eflags寄存器中設置了一個標記,如果是1我們才允許被打斷,如果是0那就算天王老子找我們也不管了。

哦不對,還有一種不可以屏蔽的中斷NMI,走得是綠色通道。不過我可不期望有這種事情發生,因為一般都沒有好事,不是電源斷電就是溫度過高,或者總線出了錯誤等這之類嚴重的事情。

8259A PIC

還有一個問題,找我們辦事兒的單位有很多,我們得要區分開來,到底是誰來消息了,而且要是他們一起來找,按什麼樣優先級順序處理,也是一件頭疼的事情。

為此,廠里單獨組建了一個全資的子公司來負責這事兒,他就是可編程中斷控制器PIC,外號8259A,其他單位想聯繫我們都得通過這個PIC,我們只需要和PIC進行對接就可以了。

我們給辦事單位都分配了一個編號,叫做中斷向量。我們還準備了一個表格叫中斷描述符表IDT,表格里記錄了很多信息,其中就有處理這个中斷號對應的函數地址。我們找PIC拿到編號后就執行處理函數就OK了。

這個表格有點大,足足有256項,咱CPU車間空間有限,放不下,就把它放在內存那傢伙那裡了,為了能快速找到這個表,專門添置了一個叫idtr的寄存器指向這個表格。

其實除了中斷,我們在執行指令的時候如果遇到了異常情況,也會去這個表裡執行異常處理函數,最常見的比如遇到了除數是0,內存地址錯誤等等情況。

這種情況下,我們必須主動放下手裡的活,去處理異常,所以我們也說異常是同步的,而中斷不知道什麼時候發生,所以是異步的。

APIC

8259A乾的挺不錯的,不過後來咱們廠擴大規模,從單核CPU變成了多核,他就有點應付不過來了。

終於有一天,廠里召開會議,把8259A給撤了,成立了一個新的全資子公司叫高級可編程中斷控制器APIC,名字就多了個高級兩個字,乾的活還是一樣的。

不過你還別說,這兩個字還真不是吹噓,比8259A不知道高到哪裡去了。

這個APIC的新公司一上台,就成立了兩個部門,一個叫I/O APIC,負責接待那些要找我們辦事兒的單位,一個叫Local APIC,以外包的形式入駐到我CPU的各個車間工作,因為就挨着我們辦公,所以取名叫Local。

I/O APIC收到中斷信號以後,根據自己的策略就分發到對應的Local APIC,咱們八個車間就可以專心處理了,為我們省了不少事兒。

不僅如此,通過這個外包團隊,我們八個車間還能向彼此發起中斷請求,我們把這個叫做處理器間中斷Inter-Processor Interrupt,簡稱IPI

中斷親和性

每當網絡中有數據包到來,網卡那傢伙就發送一个中斷消息過來,告訴我們去處理。

不過最近不知道怎麼回事,網絡數據量激增。咱們廠里明明有8個車間,他非得一個勁的只給我們發消息,搞得我們手頭的工作老是被打斷,忙得不可開交。

終於,我忍不住了,去找網卡那傢伙理論了一番。不過他告訴我,這也不能怪他,分發給誰處理,那是APIC在負責。

想想也是,回頭我就去了APIC那裡,要求他們分攤一點給別的車間處理。

APIC表示這他們做不了主,得讓廠里來決定。

沒過幾天,廠里開了個會,參會的有各車間代表、APIC負責人,還請了操作系統那邊的相關代表過來。

會上,大家為了此事爭執不休。

二號車間虎子:“阿Q,誰叫你們一號車間是Bootstrap Processor,你們就多辛苦一點嘛”

三號車間代表:“你這話說的不合適,大家是一個Team,要互相幫助!要不這樣,既然有這麼多單位要聯繫我們,咱就分下工,比如一號車間負責網卡,二號負責磁盤,我們三號負責鍵盤,以此類推”

五號車間代表:“你想的倒是挺美哦,鍵盤一天能發多少中斷,網卡一天要發多少中斷,你凈挑輕鬆的干。這樣吧,咱就用隨機分發進行負載均衡你們覺得怎麼樣?”

八號車間代表:“隨機個啥啊,多麻煩,依我看吶咱8個車間就輪流來唄”

這時,領導問操作系統代表有沒有什麼建議。

這代表站起身來,推了推眼鏡說到:“幾位有沒有聽過線程的CPU親和性?”

大家都搖了搖頭,問到:“這是個什麼意思?”

“就是有些線程想綁定在你們之中的某一個核上面執行,不希望一會兒在這個核執行,一會兒在那個核執行”

我接過他的話:“好像是有這麼回事兒,之前有遇到過,有個線程一直被分配到我們一號車間,不過我們對這個不用關心吧,執行誰不是幹活啊,對我們都一個樣”

代表搖了搖頭,“唉,這可不一樣!你們每個核的一二級緩存都是自己在管理,要是換到別的核,這緩存多半就沒用了,又得重新來建立,這換來換去的豈不是瞎耽誤功夫嘛!對於一般的線程他們倒是不關心,但是有些線程執行大量的內存訪問和運算處理,又對性能要求很高的話,那就很在意這個問題了”

我們幾個都恍然大悟,紛紛點頭。

虎子起身問到:“那你們是如何實現這個親和性的呢?這跟我們今天的會議又有什麼關係呢?”

代表繼續回答說到:“我先回答你的第一個問題。線程調度是我們操作系統完成的工作,我們提供了API接口,線程通過調用這些接口表明自己的親和性意願,我們在調度的時候就能按照他們的意願把線程分配給你們來執行。”

代表喝了一口水接着說到:“我再回答你的第二個問題。既然線程可以有親和性,那中斷也可以按照這個思路來分發啊!APIC默認有一套分發策略,但是也提供親和性的設置,可以指定誰哪些核來處理,這樣不用把規矩定死,靈活可變,豈不更好?”

剛說完,會議室門口突然出現一年輕少年,揮手將操作系統代表喚了出去。

接下來,我們詳細討論了這種方案的可行性,最後大家一致決定,就照這麼辦,我們一起提出了一個叫中斷親和性的東西,操作系統那邊提供一個可配置的入口smp_affinity,可以通過設置各處理器核的掩碼來決定中斷交由誰來處理,APIC回去負責落地支持。

有了這套方案,再遇到網絡高峰期,咱們一號車間的壓力就有辦法緩解了。

我們剛剛達成一致,操作系統代表返回會議室,神色凝重的說到:“不好意思各位,操作系統那邊有點事情需要趕回去處理一下,先走一步了”

未完待續······

彩蛋

隨着網卡的一聲中斷,一個新的數據包來到了這片土地。

帝國網絡部新來的年輕人顯然沒有意識到危險的到來······

預知後事如何,請關注後續精彩······

往期TOP5文章

真慘!連各大編程語言都擺起地攤了!

因為一個跨域請求,我差點丟了飯碗

完了!CPU一味求快出事兒了!

哈希表哪家強?幾大編程語言吵起來了!

一個HTTP數據包的奇幻之旅

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

7000 字說清楚 HashMap,面試點都在裏面了

我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。

這是上篇文章 有趣的條漫版 HashMap,25歲大爺都能看懂 的文字版。有不少同學說條漫版的比較有意思,簡單易懂,但是畢竟圖片畫不了那麼詳細,只能從大面而上理解。

真正的了解細節,還得看這一篇。其實是這篇先寫完,然後畫了不少圖片,所以就寫了一篇圖片版的。本篇 7000 多字,建議三連呦。

在 Java 中,最常用的數據類型是 8 中基本類型以及他們的包裝類型以及字符串類型,其次應該就是 ArrayListHashMap了吧。HashMap存的是鍵值對類型的數據,其存儲和獲取的速度快、性能高,是非常好用的一個數據結構,每一個 Java 開發者都肯定用過它。

而且 HashMap的設計巧妙,其結構和原理也經常被拿去當做面試題。其中有很多巧妙的算法和設計,比如 Hash 算法、拉鏈法、紅黑樹設計等,值得每一個開發者借鑒學習。

想了老半天,怎麼才能簡單易懂的把 HashMap說明白呢,那就從我理解它的思路和過程去說吧。要理解一個事物最好的方式就是先了解整體結構,再去追究細節。所以,我們先從結構談起。

先從結構說起

拿我自身的一個體會來說吧,風箏我作為一個專業路痴,對於迷路這件事兒絕不含糊,雖然在北京混跡多年,但是只在中關村能分清南北,其他地方,哪怕是我每天住的小區、每天工作的公司也分不太清方向,回家只能認一條路,要是打車換條路回家,也得迷糊一陣,這麼說吧,在小區前面能回家,小區後面找不到家。去個新地方,得盯着地圖看半天。這時,我就在想啊,要是我能在城市上空俯瞰下面的街道,那我就再也不怕找不到回家的路了。這不就是三體里的降維打擊嗎,站在高維的立場,理解低維的事物,那就簡單多了。

理解數據結構也是一個道理,大多數時候,我們都是停留在會用的層面上,理解一些原理也只是支離破碎的,困在數據機構的迷宮裡跌跌撞撞,迫切的需要一張地圖或者一架直升機。

先來看一下整個 Map家族的集成關係圖,一看東西還不少,但其他的可能都沒怎麼用過,只有 HashMap最熟悉。

以下描述可能不夠專業,只為簡單的描述 HashMap的結構,請結合下圖進行理解。

HashMap主體上就是一個數組結構,每一個索引位置英文叫做一個 bin,我們這裏先管它叫做桶,比如你定義一個長度為 8 的 HashMap,那就可以說這是一個由 8 個桶組成的數組。當我們像數組中插入數據的時候,大多數時候存的都是一個一個 Node 類型的元素,Node 是 HashMap中定義的靜態內部類。

當插入數據(也就是調用 put 方法)的時候,並不是按順序一個一個向後存儲的,HashMap中定義了一套專門的索引選擇算法,叫做散列計算,但散列計算存在一種情況,叫哈希碰撞,也就是兩個不一樣的 key 散列計算出來的 hash 值是一致的,這種情況怎麼辦呢,採用拉鏈法進行擴展,比如圖中藍色的鏈表部分,這樣一來,具有相同 hash 值的不同 key 即可以落到相同的桶中,又保證不會覆蓋之前的內容。

但隨着插入的元素越來越多,發生碰撞的概率就越大,某個桶中的鏈表就會越來越長,直到達到一個閾值,HashMap就受不了了,為了提升性能,會將超過閾值的鏈錶轉換形態,轉換成紅黑樹的結構,這個閾值是 8 。也就是單個桶內的鏈表節點數大於 8 ,就會將鏈表變身為紅黑樹。

以上概括性的描述就是 HashMap的整體結構,也是我們進一步研究細節的藍圖。我們將從中抽取出幾個關鍵點一一解釋,從整體到細節,降維打擊 HashMap

接下來就是說明為什麼會設計成這樣的結構以及從單純數組到桶內鏈表產生,接着把鏈錶轉換成紅黑樹的詳細過程。

認清幾個關鍵概念

存儲容器

因為HashMap內部是用一個數組來保存內容的,數組定義如下:

transient Node<K,V>[] table;

Node 類型

table 是一個 Node類型的數組,Node是其中定義的靜態內部類,主要包括 hash、key、value 和 next 的屬性。比如之後我們使用 put 方法像其中加鍵值對的時候,就會轉換成 Node 類型。

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  V value;
  Node<K,V> next;
}

TreeNode

前面說了,當桶內鏈表到達 8 的時候,會將鏈錶轉換成紅黑樹,就是 TreeNode類型,它也是 HashMap中定義的靜態內部類。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  TreeNode<K,V> parent;  // red-black tree links
  TreeNode<K,V> left;
  TreeNode<K,V> right;
  TreeNode<K,V> prev;    // needed to unlink next upon deletion
  boolean red;
}

容量和默認容量

容量就是 table 數組的長度,也就是我們所說的桶的個數。其定義如下

int threshold;

默認是 16,如果我們在初始化的時候沒有指定大小,那就是 16。當然我們也可以自己指定初始大小,而 HashMap 要求初始大小必須是 2 的 冪次方。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

元素個數

容量是指定了桶的個數,而 size 是說 HashMap中實際存了多少個鍵值對。

transient int size;

最大容量

table 的長度也是有限制的,不能無限大,HashMap規定最大長度為 2 的30次方。

static final int MAXIMUM_CAPACITY = 1 << 30;

負載因子

這是一個係數,它和 threshold 結合起作用,默認是 0.75。一般情況下不要改。

final float loadFactor;

擴容閾值

閾值 = 容量 x 負載因子,假設當前 HashMap的容量是 16,負載因子是默認值 0.75,那麼當 size 到達 16 x 0.75= 12 的時候,就會觸發擴容。

初始化 HashMap

使用 HashMap肯定要初始化吧,很多情況下都是用無參構造方法創建。

Map<String,String> map = new HashMap<>();

這種情況下所有屬性都是默認值,比如容量是 16,負載因子是 0.75。

另外推薦的一種初始化方式,就是給定一個默認容量,比如指定默認容量是 32。

Map<String,String> map = new HashMap<>(32);

但是 HashMap 要求初始大小必須是 2 的 n 次方,但是又不能要求每個開發人員指定初始容量的時候都按要求來,比如我們指定初始大小為為 7、18 這種會怎麼樣呢?

沒關係,HashMap中有個方法專門負責將傳過來的參數值轉換為最接近、且大於等於指定參數的 2 的 n 次方的值,比如指定大小為 7 的話,最後實際的容量就是 8 ,如果指定大小為 18的話,那最後實際的容量就是 32 。

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

執行這個轉換動作的就是 tableSizeFor方法,經過轉換后,將最終的結果賦值給 threshold變量,也就是初始容量,也就是本篇中所說的桶個數。

static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor這個方法就有意思了,先把初始參數減 1,然後連着做或等於無符號右移操作,最後算出一個接近的 2 的冪次方,下圖演示了初始參數為 18 時的一系列操作,最後得出的初始大小為 32。

這個算法很有意思了,比如你給的初始大小是 63,那得到的結果就是 64,如果初始大小給定 65 ,那得到的結果就是 128,總是能得出不小於給定初始大小,並且最接近的2的n次方的最終值。

從 put 方法解密核心原理

put方法是增加鍵值對最常用的方法,也是最複雜的過程,增加鍵值對的過程涉及了 HashMap最核心的原理,主要包括以下幾點:

  1. 什麼情況下會擴容,擴容的規則是什麼?
  2. 插入鍵值對的時候如何確定索引,HashMap可不是按順序插入的,那樣不就真成了數組了嗎。
  3. 如何確保 key 的唯一性?
  4. 發生哈希碰撞怎麼處理?
  5. 拉鏈法是什麼?
  6. 單桶內的鏈表如何轉變成紅黑樹?

以下是 put 方法的源碼,我在其中做了註釋。


public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  HashMap.Node<K,V>[] tab; // 聲明 Node 數組 tab
  HashMap.Node<K,V> p;    // 聲明一個 Node 變量 p
  int n, i;
  /**
  * table 定義 transient Node<K,V>[] table; 用來存儲 Node 節點
  * 如果 當前table為空,則調用resize() 方法分配數組空間
  */
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // n 總是為 2 的冪次方,(n-1) & hash 可確定 tab.length (也就是table數組長度)內的索引
  // 然後 創建一個 Node 節點賦給當前索引
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    //如果當前索引位置已經有值了,怎麼辦
    // 拉鏈法出場
    HashMap.Node<K,V> e;
    K k;
    // 判斷 key 值唯一性
    // p 是當前待插入索引處的值
    // 哈希值一致並且(當前位置的 key == 待插入的key(注意 == 符號),或者key 不為null 並且 key.equals(k))
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k)))) //如果當前節點只有一個元素,且和待插入key一樣 則覆蓋
      // 將 p(當前索引)節點臨時賦予 e
      e = p;
    else if (p instanceof HashMap.TreeNode) // 如果當前索引節點是一顆樹節點
      //插入節點樹中 並返回
      e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      // 當前索引節點即不是只有一個節點,也不是一顆樹,說明是一個鏈表
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) { //找到沒有 next 的節點,也就是最後一個
          // 創建一個 node 賦給 p.next
          p.next = newNode(hash, key, value, null);
          // 如果當前位置+1之後大於 TREEIFY_THRESHOLD 則要進行樹化
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            //執行樹化操作
            treeifyBin(tab, hash);
          break;
        }
        //如果又發生key衝突則停止 後續這個節點會被相同的key覆蓋
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  // 當實際長度大於 threshold 時 resize
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

首次初始化數組和擴容

在執行 put方法時,第一步要檢查 table 數組是否為空或者長度是否為 0,如果是這樣的,說明這是首次插入鍵值對,需要執行 table 數組初始化操作。

另外,隨之鍵值對添加的越來越多,HashMap的 size 越來越大,注意 size 前面說了,是實際的鍵值對數量,那麼 size 到了多少就要擴容了呢,並不是等 size 和 threshold(容量)一樣大了才擴容,而是到了閾值就開始擴容,閾值上面也說了,是容量 x 負載因子

為什麼放在一起說呢,因為首次初始化和擴容都是用的同一個方法,叫做 resize()。以下是我註釋的 resize()方法。

final HashMap.Node<K,V>[] resize() {
  // 保存 table 副本,接下來 copy 到新數組用
  HashMap.Node<K,V>[] oldTab = table;
  // 當前 table 的容量,是 length 而不是 size
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  // 當前桶大小
  int oldThr = threshold;

  int newCap, newThr = 0;
  if (oldCap > 0) { //如果當前容量大於 0,也就是非第一次初始化的情況(擴容場景下)
    if (oldCap >= MAXIMUM_CAPACITY) { //不能超過最大允許容量
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY) // 雙倍擴容
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // 初始化的場景(給定默認容量),比如 new HashMap(32)
    newCap = oldThr; //將容量設置為 threshold 的值
  else {               // 無參數初始化場景,new HashMap()
    // 容量設置為 DEFAULT_INITIAL_CAPACITY
    newCap = DEFAULT_INITIAL_CAPACITY;
    // 閾值 超過閾值會觸發擴容
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  if (newThr == 0) { //給定默認容量的初始化情況
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
  }
  // 保存新的閾值
  threshold = newThr;
  // 創建新的擴容后數組,然後將舊的元素複製過去
  @SuppressWarnings({"rawtypes","unchecked"})
  HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
  table = newTab;
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      HashMap.Node<K,V> e;
      //遍歷 獲得得到元素 賦給 e
      if ((e = oldTab[j]) != null) { //如果當前桶不為空
        oldTab[j] = null; // 置空回收
        if (e.next == null) //節點 next為空的話 重新尋找落點 
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof HashMap.TreeNode) //如果是樹節點
          //紅黑樹節點單獨處理
          ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // 保持原順序
          HashMap.Node<K,V> loHead = null, loTail = null;
          HashMap.Node<K,V> hiHead = null, hiTail = null;
          HashMap.Node<K,V> next;
          do {
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}

首次初始化

put方法中線先檢查 table 數組是否為空,如果為空就初始化。

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

首次初始化分為無參初始化和有參初始化兩種情況,前面在講 HashMap初始化的時候說了,無參情況默認就是 16,也就是 table 的長度為 16。有參初始化的時候,首先使用 tableSizeFor()方法確定實際容量,最後 new 一個 Node 數組出來。

HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];

其中 newCap就是容量,默認16或者自定義的。

而這個過程中還有很重要的一步,就是維護擴容閾值

擴容

put方法中,判斷當 size(實際鍵值對個數)到達 threshold (閾值)時,觸發擴容操作。

// 當實際長度大於 threshold 時 resize
if (++size > threshold)
    resize();

HashMap遵循兩倍擴容規則,每次擴容之後的大小是擴容前的兩倍。另外,說到底,底層的存儲還是一個數組,Java 中沒有真正的動態數組這一說,數組初始化的時候是多大,那它就一直是這麼大,那擴容是怎麼來的呢,答案就是創建一個新數組,然後將老數組的數據拷貝過去。

拷貝的時候可能會有如下幾種情況:

  1. 如果節點 next 屬性為空,說明這是一個最正常的節點,不是桶內鏈表,也不是紅黑樹,這樣的節點會重新計算索引位置,然後插入。
  2. 如果是一顆紅黑樹,則使用 split方法處理,原理就是將紅黑樹拆分成兩個 TreeNode 鏈表,然後判斷每個鏈表的長度是否小於等於 6,如果是就將 TreeNode 轉換成桶內鏈表,否則再轉換成紅黑樹。
  3. 如果是桶內鏈表,則將鏈表拷貝到新數組,保證鏈表的順序不變。

確定插入點

當我們調用 put方法時,第一步是對 key 進行 hash 計算,計算這個值是為了之後尋找落點,也就是究竟要插入到 table 數組的哪個桶中。

hash 算法是這樣的,拿到 key 的 hashCode,將 hashCode 做一次16位右位移,然後將右移的結果和 hashCode 做異或運算,這段代碼叫做「擾動函數」,之所以不直接拿 hashCode 是為了增加隨機性,減少哈希碰撞次數。

/**
* 用來計算 key 的 hash 值
**/
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

拿到這個 hash 值之後,會進行這樣的運算 i = (n - 1) & hash,其中 i就是最終計算出來的索引位置。

有兩個場景用到了這個索引計算公式,第一個場景就是 put方法插入鍵值對的時候。第二個場景是在 resize 擴容的時候,new 出來新數組之後,將已經存在的節點移動到新數組的時候,如果節點不是鏈表,也不是紅黑樹,而是一個普通的 Node 節點,會重新計算,找到在新數組中的索引位置。

接着看圖,還是圖說的清楚。

HashMap 要求容量必須是 2 的 n 次方,2的 n 次方的二進製表示大家肯定都很清楚,2的6次方,就是從右向左 6 個 0,然後第 7 位是 1,下圖展示了 2 的 6 次方的二進製表示。

然後這個 n-1的操作就厲害了,減一之後,後面之前二進製表示中 1 後面的 0 全都變成了 1,1 所在的位變為 0。比如 64-1 變為 63,其二進製表示是下面這樣的。

下圖中,前面 4 行分別列出了當 map 的容量為 8、16、32、64的時候,假設容量為 n,則對應的 n-1 的二進製表示是下面這樣的,尾部一片紅,都是 1 ,能預感到將要有什麼騷操作。

沒錯,將這樣的二進製表示代入這個公式 (n - 1) & hash中,最終就能確定待插入的索引位了。接着看圖最下面的三行,演示了假設當前 HashMap的容量為 64 ,而待插入的一個 key 經過 hash 計算后得到的結果是 99 時,代入公式計算 index 的值,也就是 (64-1)& 99,最終的計算結果是 35,也就是這個 key 會落到 table[35] 這個位置。

為什麼 HashMap一定要保證容量是 2 的冪次方呢,通過二進製表示可以看出,如果有多位是 1 ,那與 hash 值進行與運算的時候,更能保證最後散列的結果均勻,這樣很大程度上由 hash 的值來決定。

如何確保 key 的唯一性

HashMap中不允許存在相同的 key 的,那怎麼保證 key 的唯一性呢,判斷的代碼如下。

if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))

首先通過 hash 算法算出的值必須相等,算出的結果是 int,所以可以用 == 符號判斷。只是這個條件可不行,要知道哈希碰撞是什麼意思,有可能兩個不一樣的 key 最後產生的 hash 值是相同的。

並且待插入的 key == 當前索引已存在的 key,或者 待插入的 key.equals(當前索引已存在的key),注意== 和 equals 是或的關係。== 符號意味着這是同一個對象, equals 用來確定兩個對象內容相同。

如果 key 是基本數據類型,比如 int,那相同的值肯定是相等的,並且產生的 hashCode 也是一致的。

String 類型算是最常用的 key 類型了,我們都知道相同的字符串產生的 hashCode 也是一樣的,並且字符串可以用 equals 判斷相等。

但是如果用引用類型當做 key 呢,比如我定義了一個 MoonKey 作為 key 值類型

public class MoonKey {

    private String keyTile;

    public String getKeyTile() {
        return keyTile;
    }

    public void setKeyTile(String keyTile) {
        this.keyTile = keyTile;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MoonKey moonKey = (MoonKey) o;
        return Objects.equals(keyTile, moonKey.keyTile);
    }
}

然後用下面的代碼進行兩次添加,你說 size 的長度是 1 還是 2 呢?

Map<MoonKey, String> m = new HashMap<>();
MoonKey moonKey = new MoonKey();
moonKey.setKeyTile("1");
MoonKey moonKey1 = new MoonKey();
moonKey1.setKeyTile("1");
m.put(moonKey, "1");
m.put(moonKey1, "2");
System.out.println(hash(moonKey));
System.out.println(hash(moonKey1));
System.out.println(m.size());

答案是 2 ,為什麼呢,因為 MoonKey 沒有重寫 hashCode 方法,導致 moonkey 和 moonKey1 的 hash 值不可能一樣,當不重寫 hashCode 方法時,默認繼承自 Object的 hashCode 方法,而每個 Object對象的 hash 值都是獨一無二的。

划重點,正確的做法應該是加上 hashCode的重寫。

@Override
public int hashCode() {
  return Objects.hash(keyTile);
}

這也是為什麼要求重寫 equals 方法的同時,也必須重寫 hashCode方法的原因之一。 如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。有了這個基礎才能保證 HashMap或者HashSet的 key 唯一。

發生哈希碰撞怎麼辦

前面剛說了相等的對象產生的 hashCode 也要相等,但是不相等的對象使用 hash方法計算之後也有可能產生相同的值,這就叫做哈希碰撞。雖然通過算法已經很大程度上避免碰撞的發生,但是卻無法避免。

產生碰撞之後,自然得出的在 table 數組的索引(也就是桶)也是一樣的,這時,怎麼辦呢,一個桶里怎麼放多個鍵值對?

拉鏈法

文章剛開頭就提到了,HashMap可不是簡單的數組而已。當碰撞發生就坦然接收。有一種方法叫做拉鏈法,不是衣服上那種拉鏈。而是,當碰撞發生了,就在當前桶上拉一條鏈表出來,這樣解釋就合理了。

前面介紹關鍵概念的時候提到了 Node類型,裏面有個屬性叫做 next,它就是為了這種鏈表設計的,如下圖所示。node1、node2、node3都落在了同一個桶中,這時候就得用鏈表的方式處理了,node1.next = node2,node2.next = node3,這樣將鏈表串起來。而 node3.next = null,則說明這是鏈表的尾巴。

當有新元素準備插入到鏈表的時候,採用的是尾插法,而不是頭插法了,JDK 1.7 的版本採用的是頭插法,但是頭插法有個問題,就是在兩個線程執行 resize() 擴容的時候,很可能造成環形鏈表,導致 get 方法出現死循環。

鏈錶轉換成樹

鏈表不是碰撞處理的終極結構,終極結構是紅黑樹,當鏈表長度到達 8 之後,再有新元素進來,那就要開始由鏈表到紅黑樹的轉換了。方法 treeifyBin是完成這個過程的。

使用紅黑樹是出於性能方面的考慮,紅黑樹的查找速度要優於鏈表。那為什麼不是一開始就直接生成紅黑樹,而是鏈表長度大於 8 之後才升級成樹呢?

首先來說,哈希碰撞的概率還是很小的,大部分情況下都是一個桶裝一個 Node,即便發生碰撞,都碰撞到一個桶的概率那就更是少之又少了,所以鏈表長度很少有機會能到 8 ,如果鏈表長度到 8 了,那說明當前 HashMap中的元素數量已經非常大了,那這時候用紅黑樹來提高性能是可取的。而反過來,如果 HashMap總的元素很少,即便用紅黑樹對性能的提升也不大,況且紅黑樹對空間的使用要比鏈表大很多。

get 方法

T value = map.get(key);

例如通過上面的語句通過 key 獲取 value 值,是我們最常用到的方法了。

看圖理解,當調用 get方法后,第一步還是要確定索引位置,也就是我們所說的桶的位置,方法和 put方法時一樣,都是先使用 hash這個 擾動函數 確定 hash 值,然後用 (n-1) & hash獲取索引。這不廢話嗎,當然得和 put的時候一樣了,不一樣還怎麼找到正確的位置。

確定桶的位置后,會出現三種情況:

單節點類型: 也就是這個桶內只有一個鍵值對,這也在 HashMap中存在最多的類型,只要不發生哈希碰撞都是這種類型。其實 HashMap最理想的情況就是這樣,全都是這種類型就完美了。

鏈表類型: 如果發現 get 的 key 所在的是一個鏈表結構,就需要遍歷鏈表,知道找到 key 相等的 Node。

紅黑樹類型: 當鏈表長度超過 8 就轉變成紅黑樹,如果發現找到的桶是一顆紅黑樹,就使用紅黑樹專有的快速查找法查找。

另外,Map.containsKey方法其實用的就是 get方法。

remove 方法

removeputget方法類似,都是先求出 key 的 hash 值,然後 (n-1) & hash獲取索引位置,之後根據節點的類型採取不同的措施。

單節點類型: 直接將當前桶元素替換為被刪除 node.next ,其實就是 null。

鏈表類型: 如果是鏈表類型,就將被刪除 node 的前一個節點的 next 屬性設置為 node.next。

紅黑樹類型: 如果是一棵紅黑樹,就調用紅黑樹節點刪除法,這裏,如果節點數在 2~6之間,就將樹結構簡化為鏈表結構。

非線程安全

HashMap沒有做併發控制,如果想在多線程高併發環境下使用,請用 ConcurrentHashMap。同一時刻如果有多個線程同時執行 put 操作,如果計算出來的索引(桶)位置是相同的,那會造成前一個 key 被后一個 key 覆蓋。

比如下圖線程 A 和 線程 B 同時執行 put 操作,很巧的是計算出的索引都是 2,而此時,線程A 和 線程B都判斷出索引為 2 的桶是空的,然後就是插入值了,線程A先 put 進去了 key1 = 1的鍵值對,但是,緊接着線程B 又 put 進去了 key2 = 2,線程A 表示痛哭流涕,白忙活一場。最後索引為2的桶內的值是 key2=2,也就是線程A的存進去的值被覆蓋了。

總結

前面沒說,HashMap搞的這麼複雜不是白搞的,它的最大優點就是快,尤其是 get數據,是 O(1)級別的,直接定位索引位置。

HashMap不是單純的數組結構,當發生哈希碰撞時,會採用拉鏈法生成鏈表,當鏈表大於 8 的時候會轉換成紅黑樹,紅黑樹可以很大程度上提高性能。

HashMap容量必須是 2 的 n 次方,這樣設計是為了保證尋找索引的散列計算更加均勻,計算索引的公式為 (n - 1) & hash

HashMap在鍵值對數量達到擴容閾值「容量 x 負載因子」的時候進行擴容,每次擴容為之前的兩倍。擴容的過程中會對單節點類型元素進行重新計算索引位置,如果是紅黑樹節點則使用 split方法重新考量,是否將紅黑樹變為鏈表。

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

用樹的高度來建築  弗班社區找回古城區的感覺

環境資訊中心特約記者 陳文姿報導

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

宣示2050年零碳排 全球87大企業領頭邁向1.5℃目標

環境資訊中心綜合外電;姜唯 編譯;彭瑞祥 審校

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

氣候暖化 瑞士居民為消失冰川辦葬禮

摘錄自2019年9月23日公視報導

瑞士居民為阿爾卑斯山的冰川舉行了葬禮,受氣候變遷影響,這座冰川從2006年消融速度加快,現在已經消失了90%面積。

大約250個瑞士居民,22日穿著黑衣,披著黑頭紗爬了約兩小時的路程,登上海拔約2700公尺的皮措爾山山頂,為這座即將消失的冰川舉行葬禮。

瑞士蘇黎世聯邦理工學院冰川專家赫斯表示,「照目前情況來看,我們還有約4個足球場大小的冰川,但過去兩年冰川消融的速度迅速增加。」

皮措爾冰川位在瑞士境內的阿爾卑斯山,自從2006年以來,已經失去了將近90%面積,現在只剩下約兩萬6000平方公尺,不到四個足球場大小,科學家認為,冰川消融如此快速是受到氣候變遷影響,如果再不控制溫室氣體排放,這座冰川將會在2030年前完全消失。

 

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧