分類: 3C資訊

  • 021.掌握Pod-Pod調度策略

    021.掌握Pod-Pod調度策略

    一 Pod生命周期管理

    1.1 Pod生命周期

    Pod在整個生命周期過程中被系統定義了如下各種狀態。

    狀態值 描述
    Pending API Server已經創建該Pod,且Pod內還有一個或多個容器的鏡像沒有創建,包括正在下載鏡像的過程。
    Running Pod內所有容器均已創建,且至少有一個容器處於運行狀態、正在啟動狀態或正在重啟狀態。
    Succeeded Pod內所有容器均成功執行退出,且不會重啟。
    Failed Pod內所有容器均已退出,但至少有一個容器退出為失敗狀態。
    Unknown 由於某種原因無法獲取該Pod狀態,可能由於網絡通信不暢導致。

    1.2 Pod重啟策略

    Pod重啟策略(RestartPolicy)應用於Pod內的所有容器,並且僅在Pod所處的Node上由kubelet進行判斷和重啟操作。當某個容器異常退出或者健康檢查失敗時,kubelet將根據RestartPolicy的設置來進行相應操作。
    Pod的重啟策略包括Always、OnFailure和Never,默認值為Always。

    • Always:當容器失效時,由kubelet自動重啟該容器;
    • OnFailure:當容器終止運行且退出碼不為0時,由kubelet自動重啟該容器;
    • Never:不論容器運行狀態如何,kubelet都不會重啟該容器。

           kubelet重啟失效容器的時間間隔以sync-frequency乘以2n來計算,例如1/2/4/8倍等,最長延時5min,並且在成功重啟后的10min后重置該時間。

    Pod的重啟策略與控制方式關聯,當前可用於管理Pod的控制器包括ReplicationController、Job、DaemonSet及直接管理kubelet管理(靜態Pod)。
    不同控制器的重啟策略限制如下:

    • RC和DaemonSet:必須設置為Always,需要保證該容器持續運行;
    • Job:OnFailure或Never,確保容器執行完成后不再重啟;
    • kubelet:在Pod失效時重啟,不論將RestartPolicy設置為何值,也不會對Pod進行健康檢查。








    Pod包含的容器數 Pod當前的狀態 發生事件 Pod的結果狀態
    RestartPolicy=Always RestartPolicy=OnFailure RestartPolicy=Never
    包含1個容器 Running 容器成功退出 Running Succeeded Succeeded
    包含1個容器 Running 容器失敗退出 Running Running Failed
    包括兩個容器 Running 1個容器失敗退出 Running Running Running
    包括兩個容器 Running 容器被OOM殺掉 Running Running Failed

    1.3 Pod健康檢查

    對Pod的健康檢查可以通過兩類探針來檢查:LivenessProbe和ReadinessProbe。
    LivenessProbe探針:用於判斷容器是否存活(running狀態),如果LivenessProbe探針探測到容器不健康,則kubelet將殺掉該容器,並根據容器的重啟策略做相應處理。若一個容器不包含LivenessProbe探針,kubelet認為該容器的LivenessProbe探針返回值用於是“Success”。
    ReadineeProbe探針:用於判斷容器是否啟動完成(ready狀態)。如果ReadinessProbe探針探測到失敗,則Pod的狀態將被修改。Endpoint Controller將從Service的Endpoint中刪除包含該容器所在Pod的Eenpoint。
    kubelet定期執行LivenessProbe探針來診斷容器的健康狀態,通常有以下三種方式:

    • ExecAction:在容器內執行一個命令,若返回碼為0,則表明容器健康。

    示例:通過執行”cat /tmp/health”命令判斷一個容器運行是否正常。容器初始化並創建該文件,10s后刪除該文件,15s秒通過命令判斷,由於該文件已被刪除,因此判斷該容器Fail,導致kubelet殺掉該容器並重啟。

      1 [root@uk8s-m-01 study]# vi dapi-liveness.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: dapi-liveness-pod
      6   labels:
      7     test: liveness-exec
      8 spec:
      9   containers:
     10     - name: dapi-liveness
     11       image: busybox
     12       args:
     13       - /bin/sh
     14       - -c
     15       - echo ok > /tmp/health; sleep 10; rm -rf /tmp/health; sleep 600
     16       livenessProbe:
     17         exec:
     18           command:
     19           - cat
     20           - /tmp/health
     21 
     22 [root@uk8s-m-01 study]# kubectl describe pod dapi-liveness-pod

    • TCPSocketAction:通過容器的IP地址和端口號執行TCP檢查,若能建立TCP連接,則表明容器健康。

    示例:

      1 [root@uk8s-m-01 study]# vi dapi-tcpsocket.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: dapi-healthcheck-tcp
      6 spec:
      7   containers:
      8     - name: nginx
      9       image: nginx
     10       ports:
     11       - containerPort: 80
     12       livenessProbe:
     13         tcpSocket:
     14           port: 80
     15         initialDelaySeconds: 30
     16         timeoutSeconds: 1
     17 
     18 [root@uk8s-m-01 study]# kubectl create -f dapi-tcpsocket.yaml


    提示:對於每種探測方式,都需要設置如下兩個參數,其包含的含義如下:

    initialDelaySeconds:啟動容器後進行首次健康檢查的等待時間,單位為s;

    timeoutSeconds:健康檢查發送請求后等待響應的超時時間,單位為s,當超時發生時,kubelet會認為容器已經無法提供服務,將會重啟該容器。

    二 Pod調度

    Kubernetes中,Pod通常是容器的載體,一般需要通過Deployment、DaemonSet、RC、Job等對象來完成一組Pod的調度與自動控制功能。

    2.1 Depolyment/RC自動調度

    Deployment或RC的主要功能之一就是自動部署一個容器應用的多份副本,以及持續監控副本的數量,在集群內始終維持用戶指定的副本數量。
    示例:

      1 [root@uk8s-m-01 study]# vi nginx-deployment.yaml
      2 apiVersion: apps/v1beta1
      3 kind: Deployment
      4 metadata:
      5   name: nginx-deployment-01
      6 spec:
      7   replicas: 3
      8   template:
      9     metadata:
     10       labels:
     11         app: nginx
     12     spec:
     13       containers:
     14       - name: nginx
     15         image: nginx:1.7.9
     16         ports:
     17         - containerPort: 80
     18 
     19 [root@uk8s-m-01 study]# kubectl get deployments
     20 NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
     21 nginx-deployment-01   3/3     3            3           30s
     22 [root@uk8s-m-01 study]# kubectl get rs
     23 NAME                             DESIRED   CURRENT   READY   AGE
     24 nginx-deployment-01-5754944d6c   3         3         3       75s
     25 [root@uk8s-m-01 study]# kubectl get pod | grep nginx
     26 nginx-deployment-01-5754944d6c-hmcpg   1/1     Running     0          84s
     27 nginx-deployment-01-5754944d6c-mcj8q   1/1     Running     0          84s
     28 nginx-deployment-01-5754944d6c-p42mh   1/1     Running     0          84s

    2.2 NodeSelector定向調度

    當需要手動指定將Pod調度到特定Node上,可以通過Node的標籤(Label)和Pod的nodeSelector屬性相匹配。
    # kubectl label nodes <node-name> <label-key>=<label-value>
    node節點創建對應的label后,可通過在定義Pod的時候加上nodeSelector的設置實現指定的調度。
    示例:

      1 [root@uk8s-m-01 study]# kubectl label nodes 172.24.9.14 speed=io
      2 node/172.24.9.14 labeled
      3 [root@uk8s-m-01 study]# vi nginx-master-controller.yaml
      4 kind: ReplicationController
      5 metadata:
      6   name: nginx-master
      7   labels:
      8     name: nginx-master
      9 spec:
     10   replicas: 1
     11   selector:
     12     name: nginx-master
     13   template:
     14     metadata:
     15       labels:
     16         name: nginx-master
     17     spec:
     18       containers:
     19       - name: master
     20         image: nginx:1.7.9
     21         ports:
     22         - containerPort: 80
     23       nodeSelector:
     24         speed: io
     25 
     26 [root@uk8s-m-01 study]# kubectl create -f nginx-master-controller.yaml
     27 [root@uk8s-m-01 study]# kubectl get pods -o wide
     28 NAME                READY   STATUS    RESTARTS    AGE    IP            NODE
     29 nginx-master-7fjgj  1/1     Running   0           82s    172.24.9.71   172.24.9.14


    提示:可以將集群中具有不同特點的Node貼上不同的標籤,實現在部署時就可以根據應用的需求設置NodeSelector來進行指定Node範圍的調度。

    注意:若在定義Pod中指定了NodeSelector條件,但集群中不存在符合該標籤的Node,即使集群有其他可供使用的Node,Pod也無法被成功調度。

    2.3 NodeAffinity親和性調度

    親和性調度機制極大的擴展了Pod的調度能力,主要增強功能如下:

    1. 更具表達力,即更精細的力度控制;
    2. 可以使用軟限制、優先採用等限制方式,即調度器在無法滿足優先需求的情況下,會使用其他次條件進行滿足;
    3. 可以依據節點上正在運行的其他Pod的標籤來進行限制,而非節點本身的標籤,從而實現Pod之間的親和或互斥關係。

    目前有兩種節點親和力表達:
    requiredDuringSchedulingIgnoredDuringExecution:硬規則,必須滿足指定的規則,調度器才可以調度Pod至Node上(類似nodeSelector,語法不同)。
    preferredDuringSchedulingIgnoredDuringExecution:軟規則,優先調度至滿足的Node的節點,但不強求,多個優先級規則還可以設置權重值。
    IgnoredDuringExecution指:如果一個Pod所在的節點在Pod運行期間標籤發生了變化,不再符合該Pod的節點親和性需求,則系統將忽略Node上Label的變化,該Pod能繼續在該節點運行。
    示例:
    條件1:只運行在amd64的節點上;盡量運行在ssd節點上。

      1 [root@uk8s-m-01 study]# vi nodeaffinity-pod.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: with-node-affinity
      6 spec:
      7   affinity:
      8     nodeAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10         nodeSelectorTerms:
     11         - matchExpressions:
     12           - key: kubernetes.io/arch
     13             operator: In
     14             values:
     15             - amd64
     16       preferredDuringSchedulingIgnoredDuringExecution:
     17       - weight: 1
     18         preference:
     19           matchExpressions:
     20           - key: disk-type
     21             operator: In
     22             values:
     23             - ssd
     24   containers:
     25   - name: with-node-affinity
     26     image: gcr.azk8s.cn/google_containers/pause:2.0


    NodeAffinity操作語法;In、NotIn、Exists、DoesNotExist、Gt、Lt。NotIn和DoesNotExist可以實現互斥功能。
    NodeAffinity規則設置注意事項:

    • 若同時定義nodeSelector和nodeAffinity,則必須兩個條件都滿足,Pod才能最終運行指定在Node上;;
    • 若nodeAffinity指定多個nodeSelectorTerms,則只需要其中一個能夠匹配成功即可;
    • 若nodeSelectorTerms中有多個matchExpressions,則一個節點必須滿足所有matchExpressions才能運行該Pod。

    2.4 PodAffinity親和性調度

    PodAffinity根據節點上正在運行的Pod標籤而不是Node標籤來判斷和調度,要求對節點和Pod兩個條件進行匹配。
    規則描述為:若在具有標籤X的Node上運行了一個或多個符合條件Y的Pod,則Pod應該(或者不應該)運行在這個Node上。
    X通常為Node節點的機架、區域等概念,Pod是屬於某個命名空間,所以條件Y表達的是一個或全部命名空間中的一個Label Selector。
    Pod親和性定義與PodSpec的affinity字段下的podAffinity字段里,互斥性定義於同一層次的podAntiAffinity子字段中。
    舉例:

      1 [root@uk8s-m-01 study]# vi nginx-flag.yaml	#創建名為pod-flag,帶有兩個標籤的Pod
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: pod-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: kubernetes.io/hostname
     17   containers:
     18   - name: with-pod-affinity
     19     image: gcr.azk8s.cn/google_containers/pause:2.0

      1 [root@uk8s-m-01 study]# vi nginx-affinity-in.yaml	#創建定義標籤security=S1,對應如上Pod “Pod-flag”。
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: pod-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: kubernetes.io/hostname
     17   containers:
     18   - name: with-pod-affinity
     19     image: gcr.azk8s.cn/google_containers/pause:2.0
     20 
     21 [root@uk8s-m-01 study]# kubectl create -f nginx-affinity-in.yaml
     22 [root@uk8s-m-01 study]# kubectl get pods -o wide


    提示:由上Pod親和力可知,兩個Pod處於同一個Node上。

      1 [root@uk8s-m-01 study]# vi nginx-affinity-out.yaml	#創建不能與參照目標Pod運行在同一個Node上的調度策略
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: anti-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: failure-domain.beta.kubernetes.io/zone
     17     podAntiAffinity:
     18       requiredDuringSchedulingIgnoredDuringExecution:
     19       - labelSelector:
     20           matchExpressions:
     21           - key: security
     22             operator: In
     23             values:
     24             - nginx
     25         topologyKey: kubernetes.io/hostname
     26   containers:
     27   - name: anti-affinity
     28     image: gcr.azk8s.cn/google_containers/pause:2.0
     29 
     30 [root@uk8s-m-01 study]# kubectl get pods -o wide	#驗證

    2.5 Taints和Tolerations(污點和容忍)

    Taint:使Node拒絕特定Pod運行;
    Toleration:為Pod的屬性,表示Pod能容忍(運行)標註了Taint的Node。
    Taint語法:$ kubectl taint node node1 key=value:NoSchedule
    解釋:為node1加上一個Taint,該Taint的鍵為key,值為value,Taint的效果為NoSchedule。即除非特定聲明可以容忍此Taint,否則不會調度至node1上。
    Toleration示例:

      1 tolerations:
      2 - key: "key"
      3   operator: "Equal"
      4   value: "value"
      5   effect: "NoSchedule"

      1 tolerations:
      2 - key: "key"
      3   operator: "Exists"
      4   effect: "NoSchedule"

    注意:Pod的Toleration聲明中的key和effect需要與Taint的設置保持一致,並且滿足以下條件:

    • operator的值是Exists(無須指定value);
    • operator的值是Equal並且value相等;
    • 空的key配合Exists操作符能夠匹配所有的鍵和值;
    • 空的effect匹配所有的effect。


    若不指定operator,則默認值為Equal。
    taint說明:系統允許在同一個Node上設置多個taint,也可以在Pod上設置多個toleration。Kubernetes調度器處理多個taint和toleration的邏輯順序:首先列出節點中所有的taint,然後忽略pod的toleration能夠匹配的部分,剩下的沒有忽略掉的taint就是對pod的效果。以下是幾種特殊情況:
    若剩餘的taint中存在effect=NoSchedule,則調度器不會把該Pod調度到這一節點上;
    若剩餘的taint中沒有NoSchedule效果,但有PreferNoSchedule效果,則調度器會嘗試不把這個Pod指派到此節點;
    若剩餘taint的效果有NoSchedule,並且這個Pod已經在該節點上運行,則會被驅逐,若沒有在該節點上運行,也不會再被調度到該節點上。
    示例:

      1 $ kubectl taint node node1 key=value1:NoSchedule
      2 $ kubectl taint node node1 key=value1:NoExecute
      3 $ kubectl taint node node1 key=value2:NoSchedule
      4 tolerations:
      5 - key: "key1"
      6   operator: "Equal"
      7   value: "value"
      8   effect: "NoSchedule"
      9 tolerations:
     10 - key: "key1"
     11   operator: "Equal"
     12   value: "value1"
     13   effect: "NoExecute"


    釋義:此Pod聲明了兩個容忍,且能匹配Node1的taint,但是由於沒有能匹配第三個taint的toleration,因此此Pod依舊不能調度至此Node。若該Pod已經在node1上運行了,那麼在運行時設置了第3個taint,它還能繼續在node1上運行,這是因為Pod可以容忍前兩個taint。
    通常,若node加上effect=NoExecute的taint,那麼該Node上正在運行的所有無對應toleration的Pod都會被立刻驅逐,而具有相應toleration的Pod則永遠不會被驅逐。同時,系統可以給具有NoExecute效果的toleration加入一個可選的tolerationSeconds字段,表明Pod可以在taint添加到Node之後還能在此Node運行多久。

      1 tolerations:
      2 - key: "key1"
      3   operator: "Equal"
      4   value: "value"
      5   effect: "NoSchedule"
      6   tolerationSeconds: 3600

    釋義:若Pod正在運行,所在節點被加入一個匹配的taint,則這個pod會持續在該節點運行3600s后被驅逐。若在此期限內,taint被移除,則不會觸發驅逐事件。
    Taints和Tolerations常用場景:

    • 獨佔節點:

    給特定的節點運行特定應用。
    $ kubectl taint nodes 【nodename】 dedicated=groupName:NoSchedule
    同時在Pod中設置對應的toleration配合,帶有合適toleration的Pod允許同時使用其他節點一樣使用有taint的節點。

    • 具有特殊硬件設備的節點

    集群中部分特殊硬件(如安裝了GPU),則可以把不需要佔用GPU的Pod禁止在此Node上調度。

      1 $ kubectl taint nodes 【nodename】 special=true:NoSchedule
      2 $ kubectl taint nodes 【nodename】 special=true:PreferNoSchedule

    • 定義Pod驅逐行為

    NoExecute的taint對節點上正在運行的Pod有以下影響:

      1. 沒有設置toleration的pod會被立刻驅逐;
      2. 配置了對應toleration的pod,若沒有為tolerationSeconds賦值,則會一直保留在此節點中;
      3. 配置了對應toleration的pod,且為tolerationSeconds賦值,則在指定時間后驅逐。

    2.6 DaemonSet

    DaemonSet是在每個Node上調度一個Pod的資源對象,用於管理集群中每個Node僅運行一份Pod的副本實例。
    常見場景:
    在每個Node上運行一個GlusterFS存儲的Daemon進程;
    在每個Node上運行一個日誌採集程序,例如Fluentd;
    在每個Node上運行一個性能監控程序,採集該Node的運行性能數據,例如Prometheus。
    示例:

      1 [root@uk8s-m-01 study]# vi fluentd-ds.yaml
      2 apiVersion: extensions/v1beta1
      3 kind: DaemonSet
      4 metadata:
      5   name: fluentd-cloud-logging
      6   namespace: kube-system
      7   labels:
      8     k8s-app: fluentd-cloud-logging
      9 spec:
     10   template:
     11     metadata:
     12       namespace: kube-system
     13       labels:
     14         k8s-app: fluentd-cloud-logging
     15     spec:
     16       containers:
     17       - name: fluentd-cloud-logging
     18         image: gcr.azk8s.cn/google_containers/fluentd-elasticsearch:1.17
     19         resources:
     20           limits:
     21             cpu: 100m
     22             memory: 200Mi
     23         env:
     24         - name: FLUENTD_ARGS
     25           value: -q
     26         volumeMounts:
     27         - name: varlog
     28           mountPath: /var/log
     29           readOnly: false
     30         - name: containers
     31           mountPath: /var/lib/docker/containers
     32           readOnly: false
     33       volumes:
     34       - name: containers
     35         hostPath:
     36           path: /var/lib/docker/containers
     37       - name: varlog
     38         hostPath:
     39           path: /var/log

    2.7 Job批處理調度

    通過Kubernetes Job資源對象可以定義並啟動一個批處理任務,批處理任務通過并行(或者串行)啟動多個計算進程去處理一批工作項。根據批處理方式不同,批處理任務可以分為如下幾種模式:
    Job Template Expansion模式:一個Job對象對應一個待處理的Work item,有幾個work item就產生幾個獨立的Job。通常適合Work item數量少、每個Work item要處理的數據量比較大的場景。
    Queue with Pod Per Work Item模式:採用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item。此模式下,Job會啟動N個Pod,每個Pod都對應一個Work item。
    Queue with Variable Pod Count模式:採用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item,但此模式下Job啟動的數量是可變的。
    Kubernetes將Job氛圍以下三類:

    • Non-parallel Jobs

    通常一個Job只啟動一個Pod,除非Pod異常,才會重啟該Pod,一旦此Pod正常結束,Job將結束。

    • Parallel Jobs with a fixed completion count

    并行Job會啟動多個Pod,此時需要設定Job的.spec.completions參數為一個正數,當正常結束的Pod數量達至此參數設定的值后,Job結束。同時.spec.parallelism參數用來控制并行度,即同時啟動幾個Job來處理Work Item。

    • Parallel Jobs with a work queue

    任務隊列方式的并行Job需要一個獨立的Queue,Work Item都在一個Queue中存放,不能設置Job的.spec.completions參數,此時Job具有以下特性:

      1. 每個Pod都能獨立判斷和決定是否還有任務項需要處理;
      2. 如果某個Pod正常結束,則Job不會再啟動新的Pod;
      3. 如果一個Pod成功結束,則此時應該不存在其他Pod還在工作的情況。它們應該都處於即將結束、退出的狀態;
      4. 如果所有Pod都結束了,且至少有一個Pod成功結束,則整個Jod成功結束。

    2.8 Cronjob定時任務

    表達式:Minutes Hours DayofMonth Month DayofWeek Year
    Minutes:可出現”,”、”_”、”*”、”/”,有效範圍為0~59的整數;
    Hours:出現”,”、”_”、”*”、”/”,有效範圍為0~23的整數;
    DayofMonth:出現”,”、”_”、”*”、”/”、”L”、”W”、”C”,有效範圍為0~31的整數;
    Month:可出現”,”、”_”、”*”、”/”,有效範圍為1~12的整數或JAN~DEC;
    DayofWeek:出現”,”、”_”、”*”、”/”、”L”、”W”、”C”、”#”,有效範圍為1~7的整數或SUN~SAT;
    *: 表示匹配該域的任意值, 假如在Minutes域使用“*”, 則表示每分鐘都會觸發事件。
    /: 表示從起始時間開始觸發, 然後每隔固定時間觸發一次,例如在Minutes域設置為5/20, 則意味着第1次觸發在第5min時, 接下來每20min觸發一次, 將在第25min、 第45min等時刻分別觸發。
    示例:*/1 * * * * #每隔1min執行一次任務

      1 [root@uk8s-m-01 study]# vi cron.yaml
      2 apiVersion: batch/v2alpha1
      3 kind: CronJob
      4 metadata:
      5   name: hello
      6 spec:
      7   schedule: "*/1 * * * *"
      8   jobTemplate:
      9     spec:
     10       template:
     11         spec:
     12           containers:
     13           - name: hello
     14             image: busybox
     15             args:
     16             - /bin/sh
     17             - -c
     18             - date; echo Hello from the Kubernetes cluster
     19           restartPolicy: OnFailure

      1 [root@master study]# kubectl create -f cron.yaml
      2 [root@master study]# kubectl get cronjob hello
      3 NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
      4 hello   */1 * * * *   False     0        <none>          29s
      5 [root@master study]# kubectl get pods
      6 NAME                     READY   STATUS      RESTARTS   AGE
      7 hello-1573378080-zvvm5   0/1     Completed   0          68s
      8 hello-1573378140-9pmwz   0/1     Completed   0          8s
      9 [root@node1 ~]# docker logs c7					#node節點查看日誌
     10 Sun Nov 10 09:31:13 UTC 2019
     11 Hello from the Kubernetes cluster
     12 [root@master study]# kubectl get jobs				#查看任務
     13 NAME               COMPLETIONS   DURATION   AGE
     14 hello-1573378500   1/1           8s         3m7s
     15 hello-1573378560   1/1           4s         2m7s
     16 hello-1573378620   1/1           6s         67s
     17 hello-1573378680   1/1           4s         7s
     18 [root@master study]# kubectl get pods -o wide | grep hello-1573378680	#以job任務查看對應的pod
     19 [root@master study]# kubectl delete cj hello			#刪除cronjob

    2.9 初始化容器

    在很多應用場景中, 應用在啟動之前都需要進行如下初始化操作。

    • 等待其他關聯組件正確運行( 例如數據庫或某個後台服務) 。
    • 基於環境變量或配置模板生成配置文件。
    • 從遠程數據庫獲取本地所需配置, 或者將自身註冊到某个中央數據庫中。
    • 下載相關依賴包, 或者對系統進行一些預配置操作。

    示例:以Nginx應用為例, 在啟動Nginx之前, 通過初始化容器busybox為Nginx創建一個index.html主頁文件。同時init container和Nginx設置了一個共享的Volume, 以供Nginx訪問init container設置的index.html文件。

      1 [root@uk8s-m-01 study]# vi nginx-init-containers.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: nginx
      6   annotations:
      7 spec:
      8   initContainers:
      9   - name: install
     10     image: busybox
     11     command:
     12     - wget
     13     - "-O"
     14     - "/work-dir/index.html"
     15     - http://kubernetes.io
     16     volumeMounts:
     17     - name: workdir
     18       mountPath: "/work-dir"
     19   containers:
     20   - name: nginx
     21     image: nginx:1.7.9
     22     ports:
     23     - containerPort: 80
     24     volumeMounts:
     25     - name: workdir
     26       mountPath: /usr/share/nginx/html
     27   dnsPolicy: Default
     28   volumes:
     29   - name: workdir
     30     emptyDir: {}

      1 [root@uk8s-m-01 study]# kubectl get pods
      2 NAME    READY   STATUS     RESTARTS   AGE
      3 nginx   0/1     Init:0/1   0          2s
      4 [root@uk8s-m-01 study]# kubectl get pods
      5 NAME    READY   STATUS    RESTARTS   AGE
      6 nginx   1/1     Running   0          13s
      7 [root@uk8s-m-01 study]# kubectl describe pod nginx		#查看事件可知會先創建init容器,名為install


    init容器與應用容器的區別如下。
    (1) init container的運行方式與應用容器不同, 它們必須先於應用容器執行完成, 當設置了多個init container時, 將按順序逐個運行, 並且只有前一個init container運行成功后才能運行后一個init container。 當所有init container都成功運行后, Kubernetes才會初始化Pod的各種信息, 並開始創建和運行應用容器。
    (2) 在init container的定義中也可以設置資源限制、 Volume的使用和安全策略, 等等。 但資源限制的設置與應用容器略有不同。

    • 如果多個init container都定義了資源請求/資源限制, 則取最大的值作為所有init container的資源請求值/資源限制值。
    • Pod的有效(effective) 資源請求值/資源限制值取以下二者中的較大值。
      • 所有應用容器的資源請求值/資源限制值之和。
      • init container的有效資源請求值/資源限制值。
    • 調度算法將基於Pod的有效資源請求值/資源限制值進行計算,即init container可以為初始化操作預留系統資源, 即使後續應用容器無須使用這些資源。
    • Pod的有效QoS等級適用於init container和應用容器。
    • 資源配額和限制將根據Pod的有效資源請求值/資源限制值計算生效。
    • Pod級別的cgroup將基於Pod的有效資源請求/限制, 與調度機制

    一致。
    (3) init container不能設置readinessProbe探針, 因為必須在它們成功運行后才能繼續運行在Pod中定義的普通容器。在Pod重新啟動時, init container將會重新運行, 常見的Pod重啟場景如下。

    • init container的鏡像被更新時, init container將會重新運行, 導致Pod重啟。 僅更新應用容器的鏡像只會使得應用容器被重啟。
    • Pod的infrastructure容器更新時, Pod將會重啟。
    • 若Pod中的所有應用容器都終止了, 並且RestartPolicy=Always, 則Pod會重啟。

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

    【其他文章推薦】

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

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

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

    小三通海運與一般國際貿易有何不同?

    小三通快遞通關作業有哪些?

  • Spring Boot2 系列教程(二十四)Spring Boot 整合 Jpa

    Spring Boot2 系列教程(二十四)Spring Boot 整合 Jpa

    Spring Boot 中的數據持久化方案前面給大夥介紹了兩種了,一個是 JdbcTemplate,還有一個 MyBatis,JdbcTemplate 配置簡單,使用也簡單,但是功能也非常有限,MyBatis 則比較靈活,功能也很強大,據我所知,公司採用 MyBatis 做數據持久化的相當多,但是 MyBatis 並不是唯一的解決方案,除了 MyBatis 之外,還有另外一個東西,那就是 Jpa,松哥也有一些朋友在公司里使用 Jpa 來做數據持久化,本文就和大夥來說說 Jpa 如何實現數據持久化。

    Jpa 介紹

    首先需要向大夥介紹一下 Jpa,Jpa(Java Persistence API)Java 持久化 API,它是一套 ORM 規範,而不是具體的實現,Jpa 的江湖地位類似於 JDBC,只提供規範,所有的數據庫廠商提供實現(即具體的數據庫驅動),Java 領域,小夥伴們熟知的 ORM 框架可能主要是 Hibernate,實際上,除了 Hibernate 之外,還有很多其他的 ORM 框架,例如:

    • Batoo JPA
    • DataNucleus (formerly JPOX)
    • EclipseLink (formerly Oracle TopLink)
    • IBM, for WebSphere Application Server
    • JBoss with Hibernate
    • Kundera
    • ObjectDB
    • OpenJPA
    • OrientDB from Orient Technologies
    • Versant Corporation JPA (not relational, object database)

    Hibernate 只是 ORM 框架的一種,上面列出來的 ORM 框架都是支持 JPA2.0 規範的 ORM 框架。既然它是一個規範,不是具體的實現,那麼必然就不能直接使用(類似於 JDBC 不能直接使用,必須要加了驅動才能用),我們使用的是具體的實現,在這裏我們採用的實現實際上還是 Hibernate。

    Spring Boot 中使用的 Jpa 實際上是 Spring Data Jpa,Spring Data 是 Spring 家族的一個子項目,用於簡化 SQL、NoSQL 的訪問,在 Spring Data 中,只要你的方法名稱符合規範,它就知道你想幹嘛,不需要自己再去寫 SQL。

    關於 Spring Data Jpa 的具體情況,大家可以參考

    工程創建

    創建 Spring Boot 工程,添加 Web、Jpa 以及 MySQL 驅動依賴,如下:

    工程創建好之後,添加 Druid 依賴,完整的依賴如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.28</version>
        <scope>runtime</scope>
    </dependency>

    如此,工程就算創建成功了。

    基本配置

    工程創建完成后,只需要在 application.properties 中進行數據庫基本信息配置以及 Jpa 基本配置,如下:

    # 數據庫的基本配置
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    
    # JPA配置
    spring.jpa.database=mysql
    # 在控制台打印SQL
    spring.jpa.show-sql=true
    # 數據庫平台
    spring.jpa.database-platform=mysql
    # 每次啟動項目時,數據庫初始化策略
    spring.jpa.hibernate.ddl-auto=update
    # 指定默認的存儲引擎為InnoDB
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

    注意這裏和 JdbcTemplate 以及 MyBatis 比起來,多了 Jpa 配置,Jpa 配置含義我都註釋在代碼中了,這裏不再贅述,需要強調的是,最後一行配置,默認情況下,自動創建表的時候會使用 MyISAM 做表的引擎,如果配置了數據庫方言為 MySQL57Dialect,則使用 InnoDB 做表的引擎。

    好了,配置完成后,我們的 Jpa 差不多就可以開始用了。

    基本用法

    ORM(Object Relational Mapping) 框架表示對象關係映射,使用 ORM 框架我們不必再去創建表,框架會自動根據當前項目中的實體類創建相應的數據表。因此,我這裏首先創建一個 User 對象,如下:

    @Entity(name = "t_user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        @Column(name = "name")
        private String username;
        private String address;
        //省略getter/setter
    }

    首先 @Entity 註解表示這是一個實體類,那麼在項目啟動時會自動針對該類生成一張表,默認的表名為類名,@Entity 註解的 name 屬性表示自定義生成的表名。@Id 註解表示這個字段是一個 id,@GeneratedValue 註解表示主鍵的自增長策略,對於類中的其他屬性,默認都會根據屬性名在表中生成相應的字段,字段名和屬性名相同,如果開發者想要對字段進行定製,可以使用 @Column 註解,去配置字段的名稱,長度,是否為空等等。

    做完這一切之後,啟動 Spring Boot 項目,就會發現數據庫中多了一個名為 t_user 的表了。

    針對該表的操作,則需要我們提供一個 Repository,如下:

    public interface UserDao extends JpaRepository<User,Integer> {
        List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
        @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
        User maxIdUser();
    }

    這裏,自定義 UserDao 接口繼承自 JpaRepository,JpaRepository 提供了一些基本的數據操作方法,例如保存,更新,刪除,分頁查詢等,開發者也可以在接口中自己聲明相關的方法,只需要方法名稱符合規範即可,在 Spring Data 中,只要按照既定的規範命名方法,Spring Data Jpa 就知道你想幹嘛,這樣就不用寫 SQL 了,那麼規範是什麼呢?參考下圖:

    當然,這種方法命名主要是針對查詢,但是一些特殊需求,可能並不能通過這種方式解決,例如想要查詢 id 最大的用戶,這時就需要開發者自定義查詢 SQL 了。

    如上代碼所示,自定義查詢 SQL,使用 @Query 註解,在註解中寫自己的 SQL,默認使用的查詢語言不是 SQL,而是 JPQL,這是一種數據庫平台無關的面向對象的查詢語言,有點定位類似於 Hibernate 中的 HQL,在 @Query 註解中設置 nativeQuery 屬性為 true 則表示使用原生查詢,即大夥所熟悉的 SQL。上面代碼中的只是一個很簡單的例子,還有其他一些點,例如如果這個方法中的 SQL 涉及到數據操作,則需要使用 @Modifying 註解。

    好了,定義完 Dao 之後,接下來就可以將 UserDao 注入到 Controller 中進行測試了(這裏為了省事,就沒有提供 Service 了,直接將 UserDao 注入到 Controller 中)。

    @RestController
    public class UserController {
        @Autowired
        UserDao userDao;
        @PostMapping("/")
        public void addUser() {
            User user = new User();
            user.setId(1);
            user.setUsername("張三");
            user.setAddress("深圳");
            userDao.save(user);
        }
        @DeleteMapping("/")
        public void deleteById() {
            userDao.deleteById(1);
        }
        @PutMapping("/")
        public void updateUser() {
            User user = userDao.getOne(1);
            user.setUsername("李四");
            userDao.flush();
        }
        @GetMapping("/test1")
        public void test1() {
            List<User> all = userDao.findAll();
            System.out.println(all);
        }
        @GetMapping("/test2")
        public void test2() {
            List<User> list = userDao.getUserByAddressEqualsAndIdLessThanEqual("廣州", 2);
            System.out.println(list);
        }
        @GetMapping("/test3")
        public void test3() {
            User user = userDao.maxIdUser();
            System.out.println(user);
        }
    }
    

    如此之後,即可查詢到需要的數據。

    好了,本文的重點是 Spring Boot 和 Jpa 的整合,這個話題就先說到這裏。

    多說兩句

    在和 Spring 框架整合時,如果用到 ORM 框架,大部分人可能都是首選 Hibernate,實際上,在和 Spring+SpringMVC 整合時,也可以選擇 Spring Data Jpa 做數據持久化方案,用法和本文所述基本是一樣的,Spring Boot 只是將 Spring Data Jpa 的配置簡化了,因此,很多初學者對 Spring Data Jpa 覺得很神奇,但是又覺得無從下手,其實,此時可以回到 Spring 框架,先去學習 Jpa,再去學習 Spring Data Jpa,這是給初學者的一點建議。

    相關案例已經上傳到 GitHub,歡迎小夥伴們們下載:

    掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 學習資源

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

    【其他文章推薦】

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

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

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

    小三通物流營運型態?

    ※快速運回,大陸空運推薦?

  • webpack優化之玩轉代碼分割和公共代碼提取

    webpack優化之玩轉代碼分割和公共代碼提取

    前言

    開發多頁應用的時候,如果不對webpack打包進行優化,當某個模塊被多個入口模塊引用時,它就會被打包多次(在最終打包出來的某幾個文件里,它們都會有一份相同的代碼)。當項目業務越來越複雜,打包出來的代碼會非常冗餘,文件體積會非常龐大。大體積文件會增加編譯時間,影響開發效率;如果直接上線,還會拉長請求和加載時長,影響網站體驗。作為一個追求極致體驗的攻城獅,是不能忍的。所以在多頁應用中優化打包尤為必要。那麼如何優化webpack打包呢?

    一、概念

    在一切開始前,有必要先理清一下這三個概念:

    • module: 模塊,在webpack眼裡,任何可以被導入導出的文件都是一個模塊。
    • chunk: chunk是webpack拆分出來的:
      • 每個入口文件都是一個chunk
      • 通過 import、require 引入的代碼也是
      • 通過 splitChunks 拆分出來的代碼也是
    • bundle: webpack打包出來的文件,也可以理解為就是對chunk編譯壓縮打包等處理后的產出。

    二、問題分析

    首先,簡單分析下,我們剛才提到的打包問題:

    • 核心問題就是:多頁應用打包後代碼冗餘,文件體積大。
    • 究其原因就是:相同模塊在不同入口之間沒有得到復用,bundle之間比較獨立。

    弄明白了問題的原因,那麼大致的解決思路也就出來了:

    • 我們在打包的時候,應該把不同入口之間,共同引用的模塊,抽離出來,放到一個公共模塊中。這樣不管這個模塊被多少個入口引用,都只會在最終打包結果中出現一次。——解決代碼冗餘。
    • 另外,當我們把這些共同引用的模塊都堆在一個模塊中,這個文件可能異常巨大,也是不利於網絡請求和頁面加載的。所以我們需要把這個公共模塊再按照一定規則進一步拆分成幾個模塊文件。——減小文件體積。
    • 至於如何拆分,方式因人而異,因項目而異。我個人的拆分原則是:
      • 業務代碼和第三方庫分離打包,實現代碼分割;
      • 業務代碼中的公共業務模塊提取打包到一個模塊;
      • 第三方庫最好也不要全部打包到一個文件中,因為第三方庫加起來通常會很大,我會把一些特別大的庫分別獨立打包,剩下的加起來如果還很大,就把它按照一定大小切割成若干模塊。

    optimization.splitChunks

    webpack提供了一個非常好的內置插件幫我們實現這一需求:CommonsChunkPlugin。不過在 webpack4 中CommonsChunkPlugin被刪除,取而代之的是optimization.splitChunks, 所幸的是optimization.splitChunks更強大!

    三、 實現

    通過一個多頁應用的小demo,我們一步一步來實現上述思路的配置。

    demo目錄結構:

    |--public/
    |   |--a.html
    |   |--index.html
    |--src/
    |   |--a.js
    |   |--b.js
    |   |--c.js
    |   |--index.js
    |--package.json
    |--webpack.config.js

    代碼邏輯很簡單,index模塊中引用了 ab 2個模塊,a 模塊中引用了 c 模塊和 jquery庫,b 模塊中也引用了 c 模塊和 jquery 庫,c 是一個獨立的模塊沒有其他依賴。

    index.js代碼如下:

    //index.js
    import a from './a.js';
    import b from './b.js';
    function fn() {
        console.log('index-------');
    }
    fn();

    a.js代碼如下:

    //a.js
    require('./c.js');
    const $ = require('jquery')
    function fn() {
        console.log('a-------');
    }
    module.exports = fn();

    b.js代碼如下:

    //b.js
    require('./c.js');
    const $ = require('jquery')
    function fn() {
        console.log('b-------');
    }
    module.exports = fn();

    c.js代碼如下:

    //c.js
    function fn() {
        console.log('c-------');
    }
    module.exports = fn();

    1. 基本配置

    webpack先不做優化,只做基本配置,看看效果。項目配置了2個入口,搭配html-webpack-plugin實現多頁打包:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
        entry: {
            index: './src/index.js',
            a: './src/a.js'
        },
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].js'
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html',
                filename: 'index.html'
            }),
            new HtmlWebpackPlugin({
                template: './public/a.html',
                filename: 'a.html'
            })
        ]
    }

    在開發模式下運行webpack:

    可以看到,打包出兩個html和兩個體積很大的(300多K)的文件a.js,index.js

    進入dist目錄檢查js文件:

    • a.js里包含c模塊代碼和jquery代碼
    • index.js里包含a模塊、b模塊、c模塊和jquery代碼

    看,同樣的代碼cjquery被打包了2遍。

    2. 初步添加splitChunks優化配置

    首先解決相同代碼打包2次的問題,我們需要讓webpack把cjquery提取出來打包為公共模塊。

    在webpack配置文件添加splitChunks:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial'
                }
            }
        }
    }

    – cacheGroups

    • cacheGroupssplitChunks配置的核心,對代碼的拆分規則全在cacheGroups緩存組裡配置。
    • 緩存組的每一個屬性都是一個配置規則,我這裏給他的default屬性進行了配置,屬性名可以不叫default可以自己定。
    • 屬性的值是一個對象,裏面放的我們對一個代碼拆分規則的描述。

    – name

    • name:提取出來的公共模塊將會以這個來命名,可以不配置,如果不配置,就會生成默認的文件名,大致格式是index~a.js這樣的。

    – chunks

    • chunks:指定哪些類型的chunk參与拆分,值可以是string可以是函數。如果是string,可以是這個三個值之一:all, async, initialall 代表所有模塊,async代表只管異步加載的, initial代表初始化時就能獲取的模塊。如果是函數,則可以根據chunk參數的name等屬性進行更細緻的篩選。

    再次打包:

    可以看到a.js,index.js從300多K減少到6點幾K。同時增加了一個common.js文件,並且兩個打包入口都自動添加了common.js這個公共模塊:

    進入dist目錄,依次查看這3個js文件:

    • a.js里不包含任何模塊的代碼了,只有webpack生成的默認代碼。
    • index.js里同樣不包含任何模塊的代碼了,只有webpack生成的默認代碼。
    • common.js里有a,b,c,index,jquery代碼。

    發現,提是提取了,但是似乎跟我們預料的不太一樣,所有的模塊都跑到common.js里去了。

    這是因為我們沒有告訴webpack(splitChunks)什麼樣的代碼為公共代碼,splitChunks默認任何模塊都會被提取。

    – minChunks

    splitChunks是自帶默認配置的,而緩存組默認會繼承這些配置,其中有個minChunks屬性:

    • 它控制的是每個模塊什麼時候被抽離出去:當模塊被不同entry引用的次數大於等於這個配置值時,才會被抽離出去。
    • 它的默認值是1。也就是任何模塊都會被抽離出去(入口模塊其實也會被webpack引入一次)。

    我們上面沒有配置minChunks,只配置了namechunk兩個屬性,所以minChunks的默認值 1 生效。也難怪所有的模塊都被抽離到common.js中了。

    優化一下,在緩存組裡配置minChunks覆蓋默認值:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2  //模塊被引用2次以上的才抽離
                }
            }
        }
    }

    然後運行webpack

    可以看到有2個文件的大小發生了變化:common.js由314K減小到311K,index.js由6.22K增大到7.56K。

    進入dist目錄查看:

    • a.js里依然不包含任何模塊的代碼(正常,因為a作為模塊被index引入了一次,又作為入口被webpack引入了一次,所以a是有2次引用的)。
    • index.js里出現了bindex模塊的代碼了。
    • common.js里只剩a,c,和jquery模塊的代碼。

    現在我們把共同引用的模塊a, c, jquery,從aindex這兩個入口模塊里抽取到common.js里了。有點符合我們的預期了。

    3. 配置多個拆分規則

    3.1 實現代碼分離,拆分第三方庫

    接下來,我希望公共模塊common.js中,業務代碼和第三方模塊jquery能夠剝離開來。

    我們需要再添加一個拆分規則。

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            minSize: 30,  //提取出的chunk的最小大小
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2,  //模塊被引用2次以上的才抽離
                    priority: -20
                },
                vendors: {  //拆分第三方庫(通過npm|yarn安裝的庫)
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'initial',
                    priority: -10
                }
            }
        }
    }

    我給cacheGroups添加了一個vendors屬性(屬性名可以自己取,只要不跟緩存組下其他定義過的屬性同名就行,否則後面的拆分規則會把前面的配置覆蓋掉)。

    – minSize

    minSize設置的是生成文件的最小大小,單位是字節。如果一個模塊符合之前所說的拆分規則,但是如果提取出來最後生成文件大小比minSize要小,那它仍然不會被提取出來。這個屬性可以在每個緩存組屬性中設置,也可以在splitChunks屬性中設置,這樣在每個緩存組都會繼承這個配置。這裏由於我的demo中文件非常小,為了演示效果,我把minSize設置為30字節,好讓公共模塊可以被提取出來,正常項目中不用設這麼小。

    – priority

    priority屬性的值為数字,可以為負數。作用是當緩存組中設置有多個拆分規則,而某個模塊同時符合好幾個規則的時候,則需要通過優先級屬性priority來決定使用哪個拆分規則。優先級高者執行。我這裏給業務代碼組設置的優先級為-20,給第三方庫組設置的優先級為-10,這樣當一個第三方庫被引用超過2次的時候,就不會打包到業務模塊里了。

    – test

    test屬性用於進一步控制緩存組選擇的模塊,與chunks屬性的作用有一點像,但是維度不一樣。test的值可以是一個正則表達式,也可以是一個函數。它可以匹配模塊的絕對資源路徑或chunk名稱,匹配chunk名稱時,將選擇chunk中的所有模塊。我這裏用了一個正則/[\\/]node_modules[\\/]/來匹配第三方模塊的絕對路徑,因為通過npm或者yarn安裝的模塊,都會存放在node_modules目錄下。

    運行一下webpack:

    可以看到新產生了一個叫vendor.js的文件(name屬性的值),同時common.js文件體積由原來的311k減少到了861bytes!

    進入dist目錄,檢查js文件:

    • a.js里不包含任何模塊代碼。
    • common.js只包含ac模塊的代碼。
    • index.js只包含bindex模塊的代碼。
    • vendor.js只包含jquery模塊的代碼。

    現在,我們在上一步的基礎上,成功從common.js里把第三方庫jquery抽離出來放到了vendor.js里。

    3.2 拆分指定文件

    如果我們還想把項目中的某一些文件單獨拎出來打包(比如工程本地開發的組件庫),可以繼續添加拆分規則。比如我的src下有個locallib.js文件要單獨打包,假設a.js中引入了它。

    //a.js
    require('./c.js');
    require('./locallib.js');  //引入自己本地的庫
    const $ = require('jquery')
    function fn() {
        console.log('a-------');
    }
    module.exports = fn();

    可以這麼配置:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            minSize: 30,  //提取出的chunk的最小大小
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2,  //模塊被引用2次以上的才抽離
                    priority: -20
                },
                vendors: {  //拆分第三方庫(通過npm|yarn安裝的庫)
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'initial',
                    priority: -10
                },
                locallib: {  //拆分指定文件
                    test: /(src\/locallib\.js)$/,
                    name: 'locallib',
                    chunks: 'initial',
                    priority: -9
                }
            }
        }
    }

    我在緩存組下又新增了一個拆分規則,通過test正則指定我就要單獨打包src/locallib.js文件,並且把優先級設置為-9,這樣當它被多次引用時,不會進入其他拆分規則組,因為另外兩個規則的優先級都比它要低。

    運行webpack打包后:

    可以看到新產生了一個locallib.js文件。進入dist目錄查看:

    • a.js里不包含任何模塊代碼。
    • common.js只包含ac模塊的代碼。
    • index.js只包含bindex模塊的代碼。
    • vendor.js只包含jquery模塊的代碼。
    • locallib.js里只包含locallib模塊的代碼。

    現在我們又在上一步的基礎上獨立打包了一個指定的模塊locallib.js

    至此,我們就成功實現了抽離公共模塊、業務代碼和第三方代碼剝離、獨立打包指定模塊。

    對比一下,優化前,打包出來js一共有633KB:

    優化后,打包出來js一共不到330KB:

    優化打包后的文件分類清晰,體積比優化前縮小了幾乎50%,有點小完美是不是!擊掌!這還只是我舉的一個簡單例子,在實際多頁應用中,優化力度說不定還不止這麼多。

    小結

    webpack很強大,以上只是冰山一角,但是只要掌握了上述optimization.splitChunks的核心配置,我們就可以幾乎隨心所欲地按照自己的想法來拆分優化代碼控制打包文件了,是不是很酷?玩轉代碼拆分,你也可以!

    如果覺得這些依然不能滿足你的需求,還想更精(bian)細(tai)地定製打包規則,可以到查看optimization.splitChunks的更多配置。

    歡迎交流~

    本文的完整webpack配置和demo源碼可以在這裏獲取:

    歡迎轉載,轉載請註明出處:

    本文同步發表於:

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

    【其他文章推薦】

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

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

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

    台灣寄大陸海運貨物規則及重量限制?

    大陸寄台灣海運費用試算一覽表

  • QQ是怎樣創造出來的?——解密好友系統的設計

    QQ是怎樣創造出來的?——解密好友系統的設計

    本篇介紹筆者接觸的第一個後台系統,從自身見聞出發,因此涉及的內容相對比較基礎,後台大牛請自覺略過。

    什麼是好友系統?

    簡單的說,好友系統是維護用戶好友關係的系統。我們最熟悉的好友系統案例當屬QQ,實際上QQ是一款即時通訊工具,憑着好友系統沉澱了海量的好友關係鏈,從而鑄就了一個堅不可摧的商業帝國。好友系統的重要性可見一斑。

    熟悉互聯網產品的人都知道,當產品有了一定的用戶量,往往會開發一個好友系統。其主要目的是增加用戶粘性(有了好友就會常來)或者增加社區活躍度(有了好友就會多交流)。

    而我的後台開發生涯就是從這樣一個系統開始的。

    那時候,好友系統對於我們團隊大部分人來說,都是一個全新的事物,因為我們大部分人都是應屆生。整個系統的架構自然不是我們一群黃毛小孩所能創造。當年的架構圖已經找不到了,但是憑着一點記憶和多年來的經驗積累,還是可以把當年的架構勾勒出來。

     

    如圖,好友系統的架構是常見的3層結構,包括接入層、邏輯層和數據層。

    我們先從數據層講起。

    因為我們對QQ太熟悉了,我們可以很容易地列出好友系統的數據主要包括用戶資料、好友關係鏈、消息(聊天消息和系統消息)、在線狀態等。

    互聯網產品往往要面對海量的請求併發,傳統的關係型數據庫比較難滿足讀寫需求。在存儲中,一般是讀多寫少的數據才會使用MySQL等關係型數據庫,而且往往還需要增加緩存來保證性能;NoSQL(Not Only SQL)應該是目前的主流。

    對於好友系統,用戶資料和好友關係鏈都使用了kv存儲,而消息使用公司自研的tlist(可以用redis的list替代),在線狀態下面再介紹。

    接着是邏輯層

    在這個系統中複雜度最高的應該是消息服務(而這個服務我並沒有參与開發[捂臉])。

    消息服務中,消息按類型分為聊天消息和系統消息(系統消息包括加好友消息、全局tips推送等),按狀態分為在線消息和離線消息。在實現中,維護3種list:聊天消息、系統消息和離線消息。聊天消息是兩個用戶共享的,系統消息和離線消息每個用戶獨佔。當用戶在線時,聊天消息和系統消息是直接發送的;如果用戶離線,就把消息往離線消息list存入一份,等用戶再次登錄時拉取。

    這樣看來,消息服務並不複雜?其實不然,系統設計中常規的流程設計往往是比較簡單的,但是對於互聯網產品,異常情況才是常態,當把各種異常情況都考慮進來時,系統就會非常複雜。

    這個例子中,消息發送丟包是一種異常情況,怎麼保證在丟包情況下,還能正常運行就是一個不小的問題。

    常見的解決方法是收包方回復確認包,發送方如果沒收到確認包就重發。但是確認包又可能丟包,那又可以給確認包增加一個確認包,這是一個永無止境的確認。

    解決方法可以參考TCP的重傳機制。那問題來了,我們為什麼不用TCP呢?因為TCP還是比較慢的,聊天消息的可靠性沒有交易數據要求那麼高,丟幾條消息並不會造成嚴重後果,但是如果用戶每次發送消息后都要等很久才能被收到,那體驗是很差的。

    一個比較折中的方案是,收包方回復確認包,如果發送方在一定時間內沒有收到確認就重發;如果收包方收到兩個相同的包(自定義seq一樣),去重即可。

    一個面試題引發的討論:

    面試時我常常會問候選人一個問題:在分佈式系統中怎樣實現一個用戶同時只能有一個終端在線(用戶在兩個地方先後登錄賬號,后一次登錄可以把前一次登錄踢下線)?這是互聯網產品中非常基礎的一個功能,考察的是候選人基本的架構設計能力。

    設計要先從接入服務器(下稱接口機)說起。接口機是好友系統對外的窗口,主要功能是維護用戶連接、登錄鑒權、加解密數據和向後端服務透傳數據等。用戶連接好友系統,首先是連接到接口機,鑒權成功后,接口機會在內存中維護用戶session,後續的操作都是基於session進行。

    如圖所示,用戶如果嘗試登錄兩次,接口機通過session就可以將第一次的登錄踢下線,從而保證只有一個終端在線。

    問題解決了嗎?

    沒有。因為實際系統肯定不會只有一台接口機,在多台接口的情況下,上面的方法就不可行了。因為每個接口機只能維護部分用戶的session,所以如果用戶先後連接到不同的接口機,就會造成用戶多處登錄的問題。

     

    自然可以想到,解決的方法就是要維護一個用戶狀態的全局視圖。在我們的好友系統中,稱為在線狀態服務。

    在線狀態服務,顧名思義就是維護用戶的在線狀態(登錄時間、接口機IP等)的服務。用戶登錄和退出會通過接口機觸發這裏的狀態變更。因為登錄包和退出包都可能丟包,所以心跳包也用作在線狀態維護(收到一次心跳標記為在線,收不到n次心跳標記為離線)。

    一種常用的方法是,採用bitmap存儲在線狀態,具體是指在內存中分配一塊空間,32位機器上的自然數一共有4294967296個,如果用一個bit來表示一個用戶ID(例如QQ號),1代表在線,0代表離線,那麼把全部自然數存儲在內存只要4294967296 / (8 * 1024 * 1024) = 512MB(8bit = 1Byte)。當然,實現中也可以根據需要給每個用戶分配更多的bit。

    於是,踢下線功能如圖所示。

     

    用戶登錄的時候,接口機首先查找本機上是否有session,如果有則更新session,接着給在線狀態服務發送登錄包,在線狀態服務檢查用戶是否已經在線,如果在線則更新狀態信息,並向上次登錄的接口機IP發送踢下線包;接口機在收到踢下線包時會檢查包中的用戶ID是否存在session,如果存在則給客戶端發送踢下線包並刪除session。

    在實際中,踢下線功能還有很多細節問題需要注意。

    又回到用戶先後登錄同一台接口機的情況:

     

    圖中踢下線流程是正確的,但是如果步驟10和13調換了順序(在UDP傳輸中是常見的)會發生什麼?大家可以自己推演一下,後到的踢下線包會把第二次登錄的A’踢下線了。這不是我們期望的。怎麼辦呢?

    解決方法分幾個細節,①接口機在收到13號登錄成功包時,先將session A替換成session A’,然後給客戶端A發生踢下線包(避免多處存活導致互相踢下線);②踢下線包中必須包含除用戶ID外的其他標識信息,session的唯一標識應該是ID+XXX的形式(我最開始採用的是ID+LoginTime),XXX是為了區分某次的登錄;③接口機在收到踢下線包的時候只要判斷ID+XXX是否吻合來決定是否給客戶端發踢下線包。

    現實情況,問題總是千奇百怪的,好在辦法總比問題多。

    比如我在項目中遇到過接口機和在線狀態服務時間漂移(差幾秒)的情況。這樣踢下線的唯一標識就不能是用戶ID+LoginTime的形式了。可以為每次的登錄生成一個唯一的UUID解決。類似的問題還有很多,不再贅述。

    總結一下,本篇主要介紹了好友系統的整體架構和部分模塊的實現方式。分佈式系統中各個模塊的實現其實並不難,難點主要在於應對複雜網絡環境帶來的問題(如丟包、時延等)和服務器異常帶來的問題(如為了應對服務器宕機會增加服務器冗餘度,進而又會引發其它問題)。

    好友系統雖然簡單,但麻雀雖小五臟俱全,架構設計的各種技術基本都有涉及。例如分層結構、負載均衡、平行擴展、容災、服務發現、服務器開發框架等方面,後面我會在各個不同的項目中介紹這些技術,敬請期待。

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

    【其他文章推薦】

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

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

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

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

  • Cesium坐標系及坐標轉換詳解

    Cesium坐標系及坐標轉換詳解

    前言

    Cesium項目中經常涉及到模型加載、瀏覽以及不同數據之間的坐標轉換,弄明白Cesium中採用的坐標系以及各個坐標系之間的轉換,是我們邁向三維GIS大門的前提,本文詳細的介紹了Cesium中採用的兩大坐標系以及之間轉換的各種方法。

    Cesium中的坐標系

    Cesium中常用的坐標有兩種WGS84地理坐標系和笛卡爾空間坐標系,我們平時常用的以經緯度來指明一個地點就是用的WGS84坐標,笛卡爾空間坐標系常用來做一些空間位置變換如平移旋轉縮放等等。二者的聯繫如下圖。

    其中,WGS84地理坐標系包括 WGS84經緯度坐標系(沒有實際的對象)和 WGS84弧度坐標系(Cartographic);

             笛卡爾空間坐標系包括 笛卡爾空間直角坐標系(Cartesian3)、平面坐標系(Cartesian2),4D笛卡爾坐標系(Cartesian4)。

    WGS84坐標系

    World Geodetic System 1984,是為GPS全球定位系統使用而建立的坐標系統,坐標原點為地球質心,其地心空間直角坐標系的Z軸指向BIH (國際時間服務機構)1984.O定義的協議地球極(CTP)方向,X軸指向BIH 1984.0的零子午面和CTP赤道的交點,Y軸與Z軸、X軸垂直構成右手坐標系。我們平常手機上的指南針显示的經緯度就是這個坐標系下當前的坐標,進度範圍[-180,180],緯度範圍[-90,90]。

    WGS84坐標系

    Cesium目前支持兩種坐標系WGS84和WebMercator,但是在Cesium中沒有實際的對象來描述WGS84坐標,都是以弧度的方式來進行運用的也就是Cartographic類:

    new Cesium.Cartographic(longitude, latitude, height),這裏的參數也叫longitude、latitude,就是經度和緯度,計算方法:弧度= π/180×經緯度角度。

     笛卡爾空間直角坐標系(Cartesian3)

    笛卡爾空間坐標的原點就是橢球的中心,我們在計算機上進行繪圖時,不方便使用經緯度直接進行繪圖,一般會將坐標系轉換為笛卡爾坐標系,使用計算機圖形學中的知識進行繪圖。這裏的Cartesian3,有點類似於三維繫統中的Point3D對象,new Cesium.Cartesian3(x, y, z),裏面三個分量x、y、z。

    笛卡爾空間直角坐標系

    平面坐標系(Cartesian2)

    平面坐標系也就是平面直角坐標系,是一個二維笛卡爾坐標系,與Cartesian3相比少了一個z的分量,new Cesium.Cartesian2(x, y)。Cartesian2經常用來描述屏幕坐標系,比如鼠標在電腦屏幕上的點擊位置,返回的就是Cartesian2,返回了鼠標點擊位置的xy像素點分量。

    平面坐標系

    坐標轉換

    經緯度和弧度的轉換

    var radians=Cesium.Math.toRadians(degrees);//經緯度轉弧度
    var degrees=Cesium.Math.toDegrees(radians);//弧度轉經緯度

    WGS84經緯度坐標和WGS84弧度坐標系(Cartographic)的轉換

    //方法一:
    var longitude = Cesium.Math.toRadians(longitude1); //其中 longitude1為角度
    
    var latitude= Cesium.Math.toRadians(latitude1); //其中 latitude1為角度
    
    var cartographic = new Cesium.Cartographic(longitude, latitude, height);
    
    //方法二:
    var cartographic= Cesium.Cartographic.fromDegrees(longitude, latitude, height);//其中,longitude和latitude為角度
    
    //方法三:
    var cartographic= Cesium.Cartographic.fromRadians(longitude, latitude, height);//其中,longitude和latitude為弧度

    WGS84坐標系和笛卡爾空間直角坐標系(Cartesian3)的轉換

    通過經緯度或弧度進行轉換
    var position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);//其中,高度默認值為0,可以不用填寫;longitude和latitude為角度
    
    var positions = Cesium.Cartesian3.fromDegreesArray(coordinates);//其中,coordinates格式為不帶高度的數組。例如:[-115.0, 37.0, -107.0, 33.0]
    
    var positions = Cesium.Cartesian3.fromDegreesArrayHeights(coordinates);//coordinates格式為帶有高度的數組。例如:[-115.0, 37.0, 100000.0, -107.0, 33.0, 150000.0]
    
    //同理,通過弧度轉換,用法相同,具體有Cesium.Cartesian3.fromRadians,Cesium.Cartesian3.fromRadiansArray,Cesium.Cartesian3.fromRadiansArrayHeights等方法

    注意:上述轉換函數中最後均有一個默認參數ellipsoid(默認值為Ellipsoid.WGS84)。

    通過過度進行轉換

    具體過度原理可以參考上邊的注意事項。

    var position = Cesium.Cartographic.fromDegrees(longitude, latitude, height);
    var positions = Cesium.Ellipsoid.WGS84.cartographicToCartesian(position);
    var positions = Cesium.Ellipsoid.WGS84.cartographicArrayToCartesianArray([position1,position2,position3]);

    笛卡爾空間直角坐標系轉換為WGS84

    直接轉換
    var cartographic= Cesium.Cartographic.fromCartesian(cartesian3);

    轉換得到WGS84弧度坐標系后再使用經緯度和弧度的轉換,進行轉換到目標值

    間接轉換
    var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(cartesian3);
    var cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray([cartesian1,cartesian2,cartesian3]);

    平面坐標系(Cartesian2)和笛卡爾空間直角坐標系(Cartesian3)的轉換

    平面坐標系轉笛卡爾空間直角坐標系

    這裏注意的是當前的點(Cartesian2)必須在三維球上,否則返回的是undefined;通過ScreenSpaceEventHandler回調會取到的坐標都是Cartesian2。

    屏幕坐標轉場景坐標-獲取傾斜攝影或模型點擊處的坐標

    這裏的場景坐標是包含了地形、傾斜攝影表面、模型的坐標。

    通過viewer.scene.pickPosition(movement.position)獲取,根據窗口坐標,從場景的深度緩衝區中拾取相應的位置,返回笛卡爾坐標。

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var position = viewer.scene.pickPosition(movement.position);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:若屏幕坐標處沒有傾斜攝影表面、模型時,獲取的笛卡爾坐標不準,此時要開啟地形深度檢測(viewer.scene.globe.depthTestAgainstTerrain = true; //默認為false)。

    屏幕坐標轉地表坐標-獲取加載地形后對應的經緯度和高程

    這裡是地球表面的世界坐標,包含地形,不包括模型、傾斜攝影表面。

    通過viewer.scene.globe.pick(ray, scene)獲取,其中ray=viewer.camera.getPickRay(movement.position)。

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var ray = viewer.camera.getPickRay(movement.position);
         var position = viewer.scene.globe.pick(ray, viewer.scene);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:通過測試,此處得到的坐標通過轉換成wgs84后,height的為該點的地形高程值。

    屏幕坐標轉橢球面坐標-獲取鼠標點的對應橢球面位置

    這裏的橢球面坐標是參考橢球的WGS84坐標(Ellipsoid.WGS84),不包含地形、模型、傾斜攝影表面。

    通過 viewer.scene.camera.pickEllipsoid(movement.position, ellipsoid)獲取,可以獲取當前點擊視線與橢球面相交處的坐標,其中ellipsoid是當前地球使用的橢球對象:viewer.scene.globe.ellipsoid,默認為Ellipsoid.WGS84

    var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(function (movement) {
         var position = viewer.scene.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid);
         console.log(position);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    注:通過測試,此處得到的坐標通過轉換成wgs84后,height的為0(此值應該為地表坐標減去地形的高程)。

    笛卡爾空間直角坐標系轉平面坐標系

    var cartesian2= Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene,cartesian3)

    空間位置變換

    經緯度轉換到笛卡爾坐標系后就能運用計算機圖形學中的仿射變換知識進行空間位置變換如平移旋轉縮放。

    Cesium為我們提供了很有用的變換工具類:Cesium.Cartesian3(相當於Point3D)Cesium.Matrix3(3×3矩陣,用於描述旋轉變換)Cesium.Matrix4(4×4矩陣,用於描述旋轉加平移變換),Cesium.Quaternion(四元數,用於描述圍繞某個向量旋轉一定角度的變換)。

    下面舉個例子:

          一個局部坐標為p1(x,y,z)的點,將它的局部坐標原點放置到loc(lng,lat,alt)上,局部坐標的z軸垂直於地表,局部坐標的y軸指向正北,並圍繞這個z軸旋轉d度,求此時p1(x,y,z)變換成全局坐標笛卡爾坐p2(x1,y1,z1)是多少?

    var rotate = Cesium.Math.toRadians(d);//轉成弧度
    var quat = Cesium.Quaternion.fromAxisAngle(Cesium.Cartesian3.UNIT_Z, rotate); //quat為圍繞這個z軸旋轉d度的四元數
    var rot_mat3 = Cesium.Matrix3.fromQuaternion(quat);//rot_mat3為根據四元數求得的旋轉矩陣
    var v = new Cesium.Cartesian3(x, y, z);//p1的局部坐標
    var m = Cesium.Matrix4.fromRotationTranslation(rot_mat3, Cesium.Cartesian3.ZERO);//m為旋轉加平移的4x4變換矩陣,這裏平移為(0,0,0),故填個Cesium.Cartesian3.ZERO
    m = Cesium.Matrix4.multiplyByTranslation(m, v);//m = m X v
    var cart3 = ellipsoid.cartographicToCartesian(Cesium.Cartographic.fromDegrees(lng, lat, alt)); //得到局部坐標原點的全局坐標
    var m1 = Cesium.Transforms.eastNorthUpToFixedFrame(cart3);//m1為局部坐標的z軸垂直於地表,局部坐標的y軸指向正北的4x4變換矩陣
    m = Cesium.Matrix4.multiplyTransformation(m, m1);//m = m X m1
    var p2 = Cesium.Matrix4.getTranslation(m);//根據最終變換矩陣m得到p2
    console.log('x=' + p2.x + ',y=' + p2.y + ',z=' + p2.z );

    總結

    通過本文,介紹了各個坐標系間的轉換問題,在具體項目中,可結合實際需求,靈活組合解決具體的實際問題。注意,博文是參照網上相關博客及結合自己的實踐總結得來,希望本文對你有所幫助,後續會更新更多內容,感興趣的朋友可以加關注,歡迎留言交流!

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

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

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

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

    ※試算大陸海運運費!

  • 詳解JavaScript錯誤捕獲和上報流程

    詳解JavaScript錯誤捕獲和上報流程

     

     

     

     

    怎麼捕獲錯誤並且處理,是一門語言必備的知識。在JavaScript中也是如此。

    那怎麼捕獲錯誤呢?初看好像很簡單,try-catch就可以了嘛!但是有的時候我們發現情況卻繁多複雜。

    • Q1: 同步可以try-catch,但一個異步回調,比如setTimeOut里的函數還可以try-catch嗎?

    • Q2: Promise的錯誤捕獲怎麼做?

    • Q3: async/await怎麼捕獲錯誤?

    • Q4: 我能夠在全局環境下捕獲錯誤並且處理嗎?

    • Q5: React16有什麼新的錯誤捕獲方式嗎?

    • Q6: 捕獲之後怎麼上報和處理?

     

    問題有點多,我們一個一個來。

     

    Q1. 同步代碼里的錯誤捕獲方式

    在同步代碼里,我們是最簡單的,只要try-catch就完了 

    function test1 () {
      try {
        throw Error ('callback err');
      } catch (error) {
        console.log ('test1:catch err successfully');
      }
    }
    test1();

    輸出結果如下,顯然是正常的

    Q2. 普通的異步回調里的錯誤捕獲方式(Promise時代以前)

    上面的問題來了,我們還能通過直接的try-catch在異步回調外部捕獲錯誤嗎?我們試一試 

    // 嘗試在異步回調外部捕獲錯誤的結果
    function test2 () {
      try {
        setTimeout (function () {
          throw Error ('callback err');
        });
      } catch (error) {
        console.log ('test2:catch err successfully');
      }
    }
    test2(); 

    輸出

    注意這裏的Uncaught Error的文本,它告訴我們錯誤沒有被成功捕捉。

    為什麼呢? 因為try-catch的是屬於同步代碼,它執行的時候,setTimeOut內部的的匿名函數還沒有執行呢。而內部的那個匿名函數執行的時候,try-catch早就執行完了。( error的內心想法:哈哈,只要我跑的夠慢,try-catch還是追不上我!)

    但是我們簡單想一想,誒我們把try-catch寫到函數裏面不就完事了嘛!

     

     

    function test2_1 () {
      setTimeout (function () {
        try {
          throw Error ('callback err');
        } catch (error) {
          console.log ('test2_1:catch err successfully');
        }
      });
    }
    test2_1();

    輸出結果如下,告訴我們這方法可行

     

    總結下Promise時代以前,異步回調中捕獲和處理錯誤的方法

    • 在異步回調內部編寫try-catch去捕獲和處理,不要在外部哦

    • 很多異步操作會開放error事件,我們根據事件去操作就可以了

    Q3. Promise里的錯誤捕獲方式

    可通過Promise.catch方法捕獲

    function test3 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      }).catch (err => {
        console.log ('promise error');
      });
    }

    輸出結果

    >> reject方法調用和throw Error都可以通過Promise.catch方法捕獲

    function test4 () {
      new Promise ((resolve, reject) => {
        reject ('promise reject error');
      }).catch (err => {
        console.log (err);
      });
    } 

    輸出結果

     

    >> then方法中的失敗回調和Promise.catch的關係

    • 如果前面的then方法沒寫失敗回調,失敗時後面的catch是會被調用的

    • 如果前面的then方法寫了失敗回調,又沒拋出,那麼後面的catch就不會被調用了

    // then方法沒寫失敗回調
    function test5 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      })
        .then (success => {})
        .catch (err => {
          console.log ('the error has not been swallowed up');
        });
    }
    // then方法寫了失敗回調
    function test5 () {
      new Promise ((resolve, reject) => {
        throw Error ('promise error');
      })
        .then (success => {},err => {})
        .catch (err => {
          console.log ('the error has not been swallowed up');
        });
    }

    輸出分別為

    1.the error has not been swallowed up
    2.無輸出

    Q4.async/await里的錯誤捕獲方式

    對於async/await這種類型的異步,我們可以通過try-catch去解決

    async function test6 () {
      try {
        await getErrorP ();
      } catch (error) {
        console.log ('async/await error with throw error');
      }
    }
     
    function getErrorP () {
      return new Promise ((resolve, reject) => {
        throw Error ('promise error');
      });
    }
    test6();
    
    

    輸出結果如下

     

    >> 如果被await修飾的Promise因為reject調用而變化,它也是能被try-catch的

    (我已經證明了這一點,但是這裏位置不夠,我寫不下了)

    Q5.在全局環境下如何監聽錯誤

    window.onerror可以監聽全局錯誤,但是很顯然錯誤還是會拋出

    window.onerror = function (err) {
      console.log ('global error');
    };
    throw Error ('global error');

     

    輸出如下

     

    Q6.在React16以上如何監聽錯誤

    >> componentDidCatch和getDerivedStateFromError鈎子函數

    class Bar extends React.Component {
      // 監聽組件錯誤
      componentDidCatch(error, info) {
        this.setState({ error, info });
      }
      // 更新 state 使下一次渲染能夠显示降級后的 UI
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
      render() {
      }
    }

     

     

    有錯誤,那肯定要上報啊!不上報就發現不了Bug這個樣子。Sentry這位老哥就是個人才,日誌記錄又好看,每次見面就像回家一樣

     

     

    Sentry簡單介紹

    Sentry provides open-source and hosted error monitoring that helps all software
    teams discover, triage, and prioritize errors in real-time.
    One million developers at over fifty thousand companies already ship
    better software faster with Sentry. Won’t you join them?
    —— Sentry官網

     

    Sentry是一個日誌上報系統,Sentry 是一個實時的日誌記錄和匯總處理的平台。專註於錯誤監控,發現和數據處理,可以讓我們不再依賴於用戶反饋才能發現和解決線上bug。讓我們簡單看一下Sentry支持哪些語言和平台吧

     

    在JavaScript領域,Sentry的支持也可以說是面面俱到

     

    參考鏈接
    https://docs.sentry.io/platforms/ 

    Sentry的功能簡單說就是,你在代碼中catch錯誤,然後調用Sentry的方法,然後Sentry就會自動幫你分析和整理錯誤日誌,例如下面這張圖截取自Sentry的網站中

     

    在JavaScript中使用Sentry 

    1.首先呢,你當然要註冊Sentry的賬號

    這個時候Sentry會自動給你分配一個唯一標示,這個標示在Sentry里叫做 dsn

    2. 安卓模塊並使用基礎功能

    安裝@sentry/browser 

    npm install @sentry/browser

     

    在項目中初始化並使用

    import * as Sentry from '@sentry/browser';
     
    Sentry.init ({
      dsn: 'xxxx',
    });
     
    try {
      throw Error ('我是一個error');
    } catch (err) {
        // 捕捉錯誤
      Sentry.captureException (err);
    }

    3.上傳sourceMap以方便在線上平台閱讀出錯的源碼

     

    // 安裝
    $ npm install --save-dev @sentry/webpack-plugin
    $ yarn add --dev @sentry/webpack-plugin
     
    // 配置webpack
    const SentryWebpackPlugin = require('@sentry/webpack-plugin');
    module.exports = {
      // other configuration
      plugins: [
        new SentryWebpackPlugin({
          include: '.',
          ignoreFile: '.sentrycliignore',
          ignore: ['node_modules', 'webpack.config.js'],
          configFile: 'sentry.properties'
        })
      ]
    }; 

    4. 為什麼不是raven.js?

     

    // 已經廢棄,雖然你還是可以用
    var Raven = require('raven-js');
    Raven
      .config('xxxxxxxxxxx_dsn')
      .install();

     

    Sentry的核心功能總結

    捕獲錯誤

    try { aFunctionThatMightFail(); } catch (err) { Sentry.captureException(err); }

     

    設置該錯誤發生的用戶信息

    下面每個選項都是可選的,但必須 存在一個選項 才能使Sentry SDK捕獲用戶: id 

    Sentry.setUser({
        id:"penghuwan12314"
      email: "penghuwan@example.com",
      username:"penghuwan",
      ip_addressZ:'xxx.xxx.xxx.xxx'
      });

     

    設置額外數據

    Sentry.setExtra("character_name", "Mighty Fighter");
    設置作用域 
    Sentry.withScope(function(scope) {
        // 下面的set的效果只存在於函數的作用域內
      scope.setFingerprint(['Database Connection Error']);
      scope.setUser(someUser);
      Sentry.captureException(err);
    });
    // 在這裏,上面的setUser的設置效果會消失

     

    設置錯誤的分組

    整理日誌信息,避免過度冗餘 

    Sentry.configureScope(function(scope) {
      scope.setFingerprint(['my-view-function']);
    });

     

    設置錯誤的級別

    在閱讀日誌時可以確定各個bug的緊急度,確定排查的優先書序

    Sentry.captureMessage('this is a debug message', 'debug');
    //fatal,error,warning,info,debug五個值
    // fatal最嚴重,debug最輕

     

    自動記錄某些事件

    例如下面的方法,會在每次屏幕調整時完成上報 

    window.addEventListener('resize', function(event){
      Sentry.addBreadcrumb({
        category: 'ui',
        message: 'New window size:' + window.innerWidth + 'x' + window.innerHeight,
        level: 'info'
      });
    })

    Sentry實踐的運用

    根據環境設置不同的dsn

    let dsn;
      if (env === 'test') {
        dsn = '測試環境的dsn';
      } else {
        dsn =
          '正式環境的dsn';
      }
     
    Sentry.init ({
      dsn
    });

     

     

     

     

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • Mirantis 收購 Docker EE | 雲原生生態周報 Vol. 28

    Mirantis 收購 Docker EE | 雲原生生態周報 Vol. 28

    作者 | 禪鳴、進超、心水、心貴

    業界要聞

    Mirantis 是一家紮根於 OpenStack 的雲公司,最近專註於 Kubernetes。該公司剛剛收購了 Docker 的企業部門,該業務部門包括 Docker Enterprise 技術平台及所有相關的知識產權、約 400 名員工中的 300 人、750 家企業客戶以及所有企業夥伴關係。

    Project Quay 包含一系列在 Apache 2.0 和其他開源許可證下許可的開源軟件。它遵循一個帶有維護者委員會的開源治理模型。

    KubeCon + CloudNativeCon North America 2019 於 11 月 18 日 在 San Diego 正式召開。

    上游重要進展

    KEP

    主要為了解決以下問題:

    • PreSidecars 將在普通容器之前啟動,但在 init 容器之後啟動,這樣它們就可以在您的主進程開始之前準備好;
    • PostSidecars 將在普通容器之後啟動,以便它們在您的主進程啟動后可以執行某些操作,例如更新 css 文件,轉發日誌等。

    主要為了解決以下問題:

    • Client/Server 版本偏移;
    • API 擴展支持;
    • 提供更簡單的選項來與 cli 工具進行集成(例如 jq);
    • 提供與 unix cli 標準集成的接口(xargs/find -exec/globbing);
    • 保留配置註釋,結構等。

    IP地址類型分解為IPv4IPv6。並逐步棄用原有地址類型,其在 1.17 中對新的 EndpointSlices 無效,然後在 1.18 中變得完全無效。

    K8S PR

    提供一種在 --show-enable-metrcis-for-version 設置時重新註冊隱藏指標的機制。

    有兩個原因:

    • 新版本中 http.CloseNotifier 已經被廢棄;
    • 如果請求協議為 HTTP/2.x,原始代碼使用 http.CloseNotifier 的情況下,每一個 Watch 將多花費 1 個 goruntine。在大規模場景下,過多的 goruntine 對 API Server 是一個非常大的負擔和性能瓶頸。

    在 Windows 上使用 Containerd 時,將由 kubelet 管理“ C: Windows  System32  drivers  etc  hosts”文件。

    為了減少 service controller 在節點有更新時,更新 backend 的延遲。

    當提供客戶端證書證書文件后,始終保持從磁盤重新啟動證書文件以進行新連接,並在證書更改時關閉連接。

    Knative

    當前 Kubernetes 社區(Kubebuilder 和 Metacontroller)正在研究控制平面可伸縮性,認為雖然用於 Kubernetes 工作的”無服務器控制器”是一個思想實驗,但距離我們並不遠,並且在技術上也是可行的。

    開源項目推薦

    阿里雲容器服務團隊自研 CNI 網絡插件,支持 VPC 和 ENI 等。

    Vmware 開源基於 OVS 的 Kubernetes 網絡方案。

    KubeSphere 是在 Kubernetes 之上構建的以應用為中心的多租戶容器管理平台,目前已經達到 GA 狀態。

    具有硬件資源感知工作負載放置策略的 Kubernetes Container Runtime Interface 代理服務。

    本周閱讀推薦

    CRDs/controllers 是 Kubernetes 中重要的組件,它們會將集群內的各種資源調整到期望狀態。學習 Reconciling 可以幫助我們更好的理解 CRDs/controllers 是如何工作的。

    通過漫畫的形式對 Openshift 及相關產品加以介紹,比較有趣。

    隨着時間的推移,Docker 開始根植於我們的日常生活當中。然而,Docker 一切輝煌的背後,技術社區中開始有不少人認為 Docker 正一路朝着沉沒的方向前進。那麼,這樣的判斷有沒有依據?Docker 真的快要不行了嗎?或者說,這隻是技術領域當中部分小年輕們一廂情願的偏執?

    由於目前國內並沒有比較好的 Go 語言書籍,而國外的優秀書籍因為英文的緣故在一定程度上也為不少 Go 語言愛好者帶來了一些學習上的困擾。

    為了加快擴散 Go 愛好者的國內群體,譯者在完成 《The Way to Go》 一書的閱讀後,決定每天抽出一點時間來進行翻譯工作,並以開源的形式免費分享給有需要的 Go 語言愛好者。

    在 Istio 服務網格中,每個 Envoy 佔用的內存也許並不算多,但所有 sidecar 增加的內存累積起來則是一個不小的数字。在進行商用部署時,我們需要考慮如何優化並減少服務網格帶來的額外內存消耗。

    Buoyant 創始人、Service Mesh 技術的提出者、第一個 Service Mesh Linkerd 的作者 Willian Morgan 為您解析 Service Mesh 現狀。

    “ 阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

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

    【其他文章推薦】

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

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

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

    台灣海運大陸貨務運送流程

    兩岸物流進出口一站式服務

  • 從最近面試聊聊我所感受的職業天花板

    ## 特別特別嚴肅的申明 (正經的)

    未免引起誤解,標題已修改。

    我一開始寫這篇文章,也純粹是有感而發。實在沒想到會引起如此多的關注。甚至還被社區大佬翻牌。說實話,誠惶誠恐。

    再次申明一遍,我寫的也僅僅只是我個人的感受,我就是萬萬千千的普通碼農中的一個,所寫文章也僅僅是從自我角度出發。

    不具備任何普適性參考性。請大家看清標題,只是個人感受,不適用.net整個行業。尤其請大家不要拿我的經歷作為語言選擇的參考。

    。net也好,java也好,每個語言都有自己的生態,選擇語言之前請先確認自己的能力,請不要抱怨語言害了你,請先審視自己是否一直在努力進步,還是只是在隨大流。

    博主就是一個普通碼農,太高端的層面很難觸及,找工作的途徑也只有朋友內推和招聘網站。

    那麼我在這兩種途徑下所接觸到的信息,就是我目前的薪資已經到了這兩種渠道下的頂薪。所以,我自我感覺是我現在觸到我的天花板。

    那麼我向上突破的方法,要麼向社區大佬學習,努力突破到那個圈子,但是對於這個自己實在沒底,大佬的圈子太高,感覺自己能力不夠,對自己實在沒信心。

    第二種就是多語言發展,修鍊技術內功,管理內功,我覺着在努努力踮踮腳還是能夠到30K、40K月薪的小尾巴。

    最後奉勸各位園友,語言從來都不是決定自己前途的關鍵因素,更多的還是要修鍊自己的內功不斷向上突破。

    博主寫這篇文章的初衷只是想表述自己目前所面臨的職業困境,實在沒想到會有如此多的關注,用詞上的不當,給大家造成困擾,萬分抱歉。

     

    #0 前言

    入職新公司沒多久,閑來無事在博客園閑逛,看到園友分享的面試經歷,正好自己這段時間面試找工作,也挺多感想的,乾脆趁這個機會總結整理一下。

    博主13年開始實習,14年畢業。到現在也工作五六年了。今年面試最大的感受就是觸及了.net的天花板。坐標,杭州。

     #1 背景

    今年九月份從一家創業公司離職,原因么自然是公司創業失敗倒閉。

    當初以技術合伙人的身份進入,雄心勃勃,然後挨了一頓社會毒打,從此老實做人,面朝黃土背朝天,老老實實去搬磚。

    九月份出來,已經是中旬,開始刷新簡歷,準備穩坐釣魚台,等着電話信息轟炸。然後,等了两天,等了一首涼涼。直到這個時候博主才意識到,形式不對。

    我的思維還停留在兩三年前,工 作遍地,只要更新下簡歷就會有無數的面試邀請。同志門,情況變了呀,行業寒冬真不只是說說而已。

    沒辦法,只好花錢,刷新下簡歷,瀏覽崗位,主動出擊。中間接到了好幾個獵頭電話, 但特么都是java。好想吐槽一下,簡歷上.net辣么大的字,你們真的不識字么,21世紀了啊喂。

    #2 某建築類軟件公司

    主營業務:建築軟件,公司已上市。

    技術框架:.net平台,具體的不是特別了解

    招聘崗位:.net高級開發工程師

    面試:一共四輪面試。

    第一輪:就是HR了,簡單聊了下情況,為什麼離職,之前薪資多少,期望薪資多少。

    第二輪:他們某業務線的部門經理和技術主管共同面試。

    基本面試情況就是我在說他們在聽,我主要講解了項目的設計方案,使用的技術,遇到的困難,最終的解決方案。

    技術面試官就問了兩個問題,一是從.net升級到netcore中間碰到過哪些問題。

    第二個基於rabbitmq的分佈式事務是怎麼做的。

    然後他們部門經理問了些團隊管理的問題。如何做團隊成員的任務分配,有團隊成員向你提出離職或者漲薪你怎麼處理,團隊的代碼質量如果管控

    第三輪:他們的CTO,然後開始又是自我介紹。

    只好把之前的又重複一遍,巴拉巴拉。最後就問了一個分佈式事務的解決方案有那些,平時是怎麼使用的。

    最後聊了一下我的定位,就是進去是負責他們的平台架構,包括一些公用業務的架構封裝,老架構的netcore升級

    第四輪:最後是他們的公司董事長,上來又是先自我介紹。然後問了下職業規劃。

    接着就是拿着我的簡歷說這個工作跳動比較頻繁,尤其是從上一家比較大的公司跳槽到一個創業公司是基於一個什麼樣的考慮呢,感覺個人穩定性和職業性規劃都不夠。

    博主當時內心的os是黑人問號臉??????我能是基於什麼樣的考慮,我為了世界和平好不好。

    然後被大佬教育了一頓,灌輸了一些個人和公司共同體,什麼共贏發展什麼共同成長的理念。

    結果:通過,HR小姐姐來談薪資。

    只能給到20K,然後還是18k基本工資+2K的級別補貼,說是我進去之後定的級別是T3,

    然後每年三四月份和九十月份可以申請調薪調級,強制要求995?????? 我特么跳槽不漲薪就算了你還給我降薪,還995,PASS。

    #3 某醫美集團下轄子公司

    主營業務:醫美行業的sass軟件

    技術框架:GRPC

    面試:一輪,技術主管。

    招聘崗位:.net架構師

    主要問題:依賴注入的生命周期,在框架設計中的應用場景有那些。

    在技術選型時主要考慮的因素。

    在框架設計時會應用到那些設計模式,主要應用場景是什麼。

    對於netcore中間件的理解。

    應對系統高併發的解決方案。

    聊一聊對微服務的理解,基於netcore的微服務架構是怎麼設計的。

    面試結果:通過。但薪資只有20K,哎呦喂,你都對不起你招聘崗位的名字呀。

    #4 某物業管理軟件公司

    主營業務:做小區物業管理軟件,公司兩百多人。

    技術框架:.net mvc 三層

    招聘崗位:.net副總監

    面試:一輪。總監面試,但是木有問任何技術問題,也木有問任何團隊管理問題。逮者我之前的離職原因各種問。

    面試結果:未通過。一臉懵逼的出來,都不知道為啥沒通過。老子也是信了你的邪。

    #5 某電商初創企業。

    主營業務:拍賣類的電商平台。公司是初創,技術團隊都沒組建完整。

    面試:兩輪。

    第一輪是他們的一個技術負責人,只是看看了簡歷,然後問了一個讓我哭笑不得問題,就是如果你進入公司,發現周圍人技術都比較菜的時候,你是不是會看不起別人。 笑哭!!!

    第二輪是老闆,老闆就是主要負責畫大餅,聊前景,聊機遇。

    結果:通過。工資待遇給到稅前24K。

    但是我了解到老闆之前做互金,然後平台清盤。具體情況不清楚,大佬,惹不起,躲了躲了。

    在這裏一定奉勸各位園友,互金平台或者老闆有互金背景的千萬小心。

    我身邊已經不少朋友,被坑到,即使現在沒事,也說不定什麼時候就會被警察找上門。

    就有朋友,剛入職公司沒多久,而且公司業務也不是做互金的,結果沒幾天,警察上門,老闆帶走就因為老闆之前做互金,還是出事兒了。

    #6 某社交類公司

    主營業務:付費社交app,主打東南亞市場

    技術框架:.net 三層

    招聘崗位:.net高級開發工程師

    面試:三輪。

    第一輪:部門的CTO面試,互相聊得挺愉快。

    主要問了之前的項目微服務怎麼做的,服務拆分的粒度怎麼規劃,整個服務的架構怎麼規劃用到哪些技術。

    然後問了數據庫方面的分庫分表怎麼做的,用的什麼中間件,分庫分表後主鍵id如何生成。

    應對高併發架構上是怎麼處理的。如何保證redis的高併發高可用。面對緩存穿透、雪崩、擊穿怎麼解決的。

    消息隊列的高可用、消息的冪等性,面對消息積壓如何處理。

    接着就是聊團隊管理,還是人員管理,任務分配,質量保證這些問題。

    接手一個新團隊后如何摸清各成員能力,不同能力的人工作上應該怎麼安排。

    還有一個,就是你作為團隊主管你的工作時間是碎片化的,但同時你作為技術leader又要把控技術方案,而做技術是需要時間的連續性,你如何協調這兩者之間的衝突。

    挺有意思,只有技術管理一肩挑的團隊才會遇到這種問題了。

    最後介紹了一下團隊目前的組織架構,技術方向。嗯,要做.net升級,要做微服務。嗯,最後要轉java。誒,是不是有什麼奇怪的東西,.netcore它不香么。

    第二輪:人事面試。嗯,就是問問離職原因,然後介紹了下公司業務發展,前景規劃,入職后的主要工作職能,然後談了下期望薪資。

    第三輪:boos面。老闆,沒問什麼問題,就是聊了聊職業規劃,然後么他介紹公司發展方向,前景規劃,我作為一個負責任的捧哏, 當然舔着嘍。

    面試結果:通過。薪資談到稅前24K。但五險一金都是最低標準繳納。年終獎說是0到12個月,看績效。

    #7 某汽車製造公司的外包崗

    面試:外包公司有個技術經理做了一個簡單電話面試。然後就約着到甲方的公司進行面試。面試兩輪,是甲方的兩個平台架構師。問題都大同小異,不贅述了。

    面試結果:通過。但博主內心相當糾結,因為對於外包,網上實在是沒有好的評價,但是和兩個面試官聊得蠻愉快。

    當初去面試了,也純粹是因為好奇,反正當時面試邀請也少,閑着也是閑着么。

    薪資談到23k,對方說還是走了一個特別申請,甲方那邊兒再高給不了。五險一金都是最低標準。

    但是HR說這個崗位是甲方為了儲備人才招聘的,我當天面試過後,甲方就把這個崗位招聘關了,只招我一個,等到明年三四月份內部編製出來,我是妥妥轉到甲方。

    而且進去之後的工作也是和面試我的那個架構師一起工作,負責他們平台架構規劃。

    一開始去面試之前我都說了工資要求和最低標準,滿口說沒問題,結果面試完了就又不行了。你個糟老頭子,壞的很,我信你個鬼。

    #8 寫在最後

      中間也還有面試有其他幾家公司,套路問題都差不多,就不在寫出來了。找工作一共花費兩周時間,面試了也有八九家,但真正能給到期望工資的就那麼兩三家。這之間自己在網上主動投遞過,但基本都沒有回信。兩周過去,在回過頭來看,卻發現網上再找不到其他合適的崗位了,不是已經面試過,就是投遞了沒反應。到最後發現,我能選擇的就只有那麼幾家公司。而且,最嚴重的一個感受就是,我翻遍了所有的招聘網站,我目前所要的工資,已經是.net行業的天花板,往上沒有空間了。.net高級開發也好、.net架構師也好、技術經理也罷,能給到工資25K就已經是到頂了,而且崗位特別少。然後做cs方向的,價格開的比bs方向的還能高一些,頂薪能到三萬。做服務的.net被java搶佔了太多市場,即便有很多公司,初期是用.net做的,即便現在netcore已經跨平台,但公司做微服務還是要轉java,我真的好想問一句netcore它不香么,vs它不香么,都咋想的。

    #9 尾篇

      最後的最後。整理一下博主在做netcore微服務所用到的相關技術,做個整體的總結。後續會一點一點具體介紹,希望能形成一個系列,希望最後能堅持寫完。

      服務註冊/發現:consul或zookeeper,各有優劣,個人傾向consul

      分佈式通訊:restful api形式或rpc。

      分佈式事件總線:推薦使用cap。cap同時支持 RabbitMQ,Kafka,Azure Service Bus 等進行底層之間的消息發送,同時內置了TCC實現。

           網關、熔斷、降級、限流:ocelot網關,應該是當下netcore平台下最火熱的網關開源項目了。同時集成了polly來滿足熔斷、降級、限流的功能要求。

      配置中心:攜程的開源項目Apollo。博主之前是為了業務需求自己寫的,不具通用性。

      微服務監控:分佈式調用鏈跟蹤zipkin和skywalking,同時還可監控服務性能。推薦使用skywalking,對代碼無侵入。

            日誌監控ELK,這個不需要多介紹了,文章太多了。

      持續集成自動部署:GitLab+Jenkins+k8s

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

    【其他文章推薦】

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

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

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

    小三通海運與一般國際貿易有何不同?

    小三通快遞通關作業有哪些?

  • .NET Core 3 WPF MVVM框架 Prism系列之數據綁定

    .NET Core 3 WPF MVVM框架 Prism系列之數據綁定

     一.安裝Prism

     

    1.使用程序包管理控制台

    Install-Package Prism.Unity -Version 7.2.0.1367

    也可以去掉‘-Version 7.2.0.1367’獲取最新的版本

     2.使用管理解決方案的Nuget包

     

    在上面或許我們有個疑問?為啥安裝prism會跟Prism.Unity有關係,我們知道Unity是個IOC容器,而Prism本身就支持IOC,且目前官方支持幾種IOC容器:

    1.且unity由於是微軟官方的,且支持prism的組件化,由此我推薦使用prism.unity,在官方文檔中prism7不支持prism.Mef,Prism 7.1將不支持prism.Autofac 2.安裝完prism.unity就已經包含着所有prism的核心庫了,架構如下:

    二.實現數據綁定

    我們先創建Views文件夾和ViewModels文件夾,將MainWindow放在Views文件夾下,再在ViewModels文件夾下面創建MainWindowViewModel類,如下:

     

    xmal代碼如下:

    <Window x:Class="PrismSample.Views.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:prism="http://prismlibrary.com/"
            xmlns:local="clr-namespace:PrismSample"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" prism:ViewModelLocator.AutoWireViewModel="True">
        <StackPanel>
            <TextBox Text="{Binding Text}" Margin="10" Height="100" FontSize="50" Foreground="Black" BorderBrush="Black"/>
            <Button  Height="100" Width="300" Content="Click Me" FontSize="50" Command="{Binding ClickCommnd}"/>
        </StackPanel>
    </Window>

     

    ViewModel代碼如下:

    using Prism.Commands;
    using Prism.Mvvm;
    
    namespace PrismSample.ViewModels
    {
       public class MainWindowViewModel:BindableBase
        {
            private string _text;
            public string Text
            {
                get { return _text; }
                set { SetProperty(ref _text, value); }
            }
    
            private DelegateCommand _clickCommnd;
            public DelegateCommand ClickCommnd =>
                _clickCommnd ?? (_clickCommnd = new DelegateCommand(ExecuteClickCommnd));
    
            void ExecuteClickCommnd()
            {
                this.Text = "Click Me!";
            }
    
            public MainWindowViewModel()
            {
                this.Text = "Hello Prism!";
            }
        }
    }

     

    啟動程序:

      點擊 click Me 按鈕:

    可以看到,我們已經成功的用prism實現數據綁定了,且View和ViewModel完美的前後端分離

    但是現在我們又引出了另外一個問題,當我們不想按照prism的規定硬要將View和ViewModel放在Views和ViewModels裏面,又或許自己的項目取名規則各不相同怎麼辦,這時候就要用到另外幾種方法:

    1.更改命名規則

    如果,公司命名規則很變態,導致項目結構變成這樣(這種公司辭職了算了):

    首先我們在App需要引入prism,修改‘Application’為‘prism:PrismApplication’且刪除StartupUri xmal代碼如下:  

    <prism:PrismApplication x:Class="PrismSample.App"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:prism="http://prismlibrary.com/"
                 xmlns:local="clr-namespace:PrismSample">
        <Application.Resources>
             
        </Application.Resources>
    </prism:PrismApplication>

      cs後台代碼如下:

    using Prism.Unity;
    using Prism.Ioc;
    using Prism.Mvvm;
    using System.Windows;
    using PrismSample.Viewsb;
    using System;
    using System.Reflection;
    
    namespace PrismSample
    {
        /// <summary>
        /// Interaction logic for App.xaml
        /// </summary>
        public partial class App : PrismApplication
        {
            //設置啟動起始頁
            protected override Window CreateShell()
            {
                return Container.Resolve<MainWindow>();
            }
    
            protected override void RegisterTypes(IContainerRegistry containerRegistry)
            {
    
            }
    
            //配置規則
            protected override void ConfigureViewModelLocator()
            {
                base.ConfigureViewModelLocator();
                ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
                {
                    var viewName = viewType.FullName.Replace(".Viewsb.", ".ViewModelsa.OhMyGod.");
                    var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
                    var viewModelName = $"{viewName}Test, {viewAssemblyName}";
                    return Type.GetType(viewModelName);
                });
            }
        }
    }

     

    上面這兩句是關鍵:

    “.Viewsb.” 表示View所在文件夾namespace,”.ViewModelsa.OhMyGod.” 表示ViewModel所在namespace

    var viewName = viewType.FullName.Replace(".Viewsb.", ".ViewModelsa.OhMyGod.");
    

      

    Test表示ViewModel後綴

    var viewModelName = $"{viewName}Test, {viewAssemblyName}";

     

    2.自定義ViewModel註冊

    我們新建一個Foo類作為自定義類,代碼如下:

    using Prism.Commands;
    using Prism.Mvvm;
    
    namespace PrismSample
    {
       public class Foo:BindableBase
        {
    
            private string _text;
            public string Text
            {
                get { return _text; }
                set { SetProperty(ref _text, value); }
            }
    
            public Foo()
            {
                this.Text = "Foo";
            }
    
            private DelegateCommand _clickCommnd;
            public DelegateCommand ClickCommnd =>
                _clickCommnd ?? (_clickCommnd = new DelegateCommand(ExecuteClickCommnd));
    
            void ExecuteClickCommnd()
            {
                this.Text = "Oh My God!";
            }
        }
    }

     

    修改App.cs代碼:

    protected override void ConfigureViewModelLocator()
            {
                base.ConfigureViewModelLocator();
                ViewModelLocationProvider.Register<MainWindow, Foo>();
                //ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
                //{
                //    var viewName = viewType.FullName.Replace(".Viewsb.", ".ViewModelsa.OhMyGod.");
                //    var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
                //    var viewModelName = $"{viewName}Test, {viewAssemblyName}";
                //    return Type.GetType(viewModelName);
                //});
            }

     

      運行:   點擊按鈕:  

    就算是不註釋修改命名規則的代碼,我們發現運行結果還是一樣,因此我們可以得出結論,

    這種直接的,不通過反射註冊的自定義註冊方式優先級會高點,在官方文檔也說明這種方式效率會高點

    且官方提供4種方式,其餘三種的註冊方式如下:

    ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(MainWindowTest)); 
    ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<Foo>());
    ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<Foo>());

     

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

    【其他文章推薦】

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

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

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

    小三通物流營運型態?

    ※快速運回,大陸空運推薦?

  • 【併發編程】synchronized的使用場景和原理簡介

    【併發編程】synchronized的使用場景和原理簡介

    1. synchronized使用

    1.1 synchronized介紹

    在多線程併發編程中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖。但是,隨着Java SE 1.6對synchronized進行了各種優化之後,有些情況下它就並不那麼重了。

    synchronized可以修飾普通方法,靜態方法和代碼塊。當synchronized修飾一個方法或者一個代碼塊的時候,它能夠保證在同一時刻最多只有一個線程執行該段代碼。

    • 對於普通同步方法,鎖是當前實例對象(不同實例對象之間的鎖互不影響)。

    • 對於靜態同步方法,鎖是當前類的Class對象。

    • 對於同步方法塊,鎖是Synchonized括號里配置的對象。

    當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。

    1.2 使用場景

    synchronized最常用的使用場景就是多線程併發編程時線程的同步。這邊還是舉一個最常用的列子:多線程情況下銀行賬戶存錢和取錢的列子。

    public class SynchronizedDemo {
    
    
        public static void main(String[] args) {
            BankAccount myAccount = new BankAccount("accountOfMG",10000.00);
            for(int i=0;i<100;i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            int var = new Random().nextInt(100);
                            Thread.sleep(var);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        double deposit = myAccount.deposit(1000.00);
                        System.out.println(Thread.currentThread().getName()+" balance:"+deposit);
                    }
                }).start();
            }
            for(int i=0;i<100;i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            int var = new Random().nextInt(100);
                            Thread.sleep(var);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        double deposit = myAccount.withdraw(1000.00);
                        System.out.println(Thread.currentThread().getName()+" balance:"+deposit);
    
                    }
                }).start();
            }
        }
    
        private static class BankAccount{
            String accountName;
            double balance;
    
            public BankAccount(String accountName,double balance){
                this.accountName = accountName;
                this.balance = balance;
            }
    
            public double deposit(double amount){
                balance = balance + amount;
                return balance;
            }
    
            public double  withdraw(double amount){
                balance = balance - amount;
                return balance;
            }
    
        }
    }

    上面的列子中,首先初始化了一個銀行賬戶,賬戶的餘額是10000.00,然後開始了200個線程,其中100個每次向賬戶中存1000.00,另外100個每次從賬戶中取1000.00。如果正常執行的話,賬戶中應該還是10000.00。但是我們執行多次這段代碼,會發現執行結果基本上都不是10000.00,而且每次結果 都是不一樣的。

    出現上面這種結果的原因就是:在多線程情況下,銀行賬戶accountOfMG是一個共享變量,對共享變量進行修改如果不做線程同步的話是會存在線程安全問題的。比如說現在有兩個線程同時要對賬戶accountOfMG存款1000,一個線程先拿到賬戶的當前餘額,並且將餘額加上1000。但是還沒將餘額的值刷新回賬戶,另一個線程也來做相同的操作。此時賬戶餘額還是沒加1000之前的值,所以當兩個線程執行完畢之後,賬戶加的總金額還是只有1000。

    synchronized就是Java提供的一種線程同步機制。使用synchronized我們可以非常方便地解決上面的銀行賬戶多線程存錢取錢問題,只需要使用synchronized修飾存錢和取錢方法即可:

    private static class BankAccount{
            String accountName;
            double balance;
    
            public BankAccount(String accountName,double balance){
                this.accountName = accountName;
                this.balance = balance;
            }
            //這邊給出一個編程建議:當我們對共享變量進行同步時,同步代碼塊最好在共享變量中加
            public synchronized double deposit(double amount){
                balance = balance + amount;
                return balance;
            }
            
            public synchronized double  withdraw(double amount){
                balance = balance - amount;
                return balance;
            }
    
        }

    2. Java對象頭

    上面提到,當線程進入synchronized方法或者代碼塊時需要先獲取鎖,退出時需要釋放鎖。那麼這個鎖信息到底存在哪裡呢?

    Java對象保存在內存中時,由以下三部分組成:

    • 對象頭
    • 實例數據
    • 對齊填充字節

    而對象頭又由下面幾部分組成:

    • Mark Word
    • 指向類的指針
    • 數組長度(只有數組對象才有)

    1. Mark Word
    Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

    Mark Word在不同的鎖狀態下存儲的內容不同,在32位JVM中是這麼存的:

    其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。Epoch是指偏向鎖的時間戳。

    JDK1.6以後的版本在處理同步鎖時存在鎖升級的概念,JVM對於同步鎖的處理是從偏向鎖開始的,隨着競爭越來越激烈,處理方式從偏向鎖升級到輕量級鎖,最終升級到重量級鎖。

    JVM一般是這樣使用鎖和Mark Word的:

    • step1:當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標誌位是01,是否偏向鎖那一位是0。

    • step2:當對象被當做同步鎖並有一個線程A搶到了鎖時,鎖標誌位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。

    • step3:當線程A再次試圖來獲得鎖時,JVM發現同步鎖對象的標誌位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經獲得了這個偏向鎖,可以執行同步鎖的代碼。

    • step4:當線程B試圖獲得這個鎖時,JVM發現同步鎖處於偏向狀態,但是Mark Word中的線程id記錄的不是B,那麼線程B會先用CAS操作試圖獲得鎖,這裏的獲得鎖操作是有可能成功的,因為線程A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word里的線程id改為線程B的id,代表線程B獲得了這個偏向鎖,可以執行同步鎖代碼。如果搶鎖失敗,則繼續執行步驟5。

    • step5:偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級為輕量級鎖。JVM會在當前線程的線程棧中開闢一塊單獨的空間,裏面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標誌位改成00,可以執行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

    • step6:輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啟用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖代碼,如果失敗則繼續執行步驟7。

    • step7:自旋鎖重試之後如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標誌位改為10。在這個狀態下,未搶到鎖的線程都會被阻塞。

    2. 指向類的指針
    該指針在32位JVM中的長度是32bit,在64位JVM中長度是64bit。Java對象的類數據保存在方法區。

    3. 數組長度
    只有數組對象保存了這部分數據。該數據在32位和64位JVM中長度都是32bit。

    synchronized對鎖的優化

    Java 6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”的概念。在Java 6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖。

    在聊偏向鎖、輕量級鎖和重量級鎖之前我們先來聊下鎖的宏觀分類。鎖從宏觀上來分類,可以分為悲觀鎖與樂觀鎖。注意,這裏說的的鎖可以是數據庫中的鎖,也可以是Java等開發語言中的鎖技術。悲觀鎖和樂觀鎖其實只是一類概念(對某類具體鎖的總稱),不是某種語言或是某個技術獨有的鎖技術。

    樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。數據庫中的共享鎖也是一種樂觀鎖。

    悲觀鎖是就是悲觀思想,即認為寫多,遇到併發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中典型的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如ReentrantLock。數據庫中的排他鎖也是一種悲觀鎖。

    偏向鎖

    Java 6之前的synchronized會導致爭用不到鎖的線程進入阻塞狀態,線程在阻塞狀態和runnbale狀態之間切換是很耗費系統資源的,所以說它是java語言中一個重量級的同步操縱,被稱為重量級鎖。為了緩解上述性能問題,Java 6開始,引入了輕量鎖與偏向鎖,默認啟用了自旋,他們都屬於樂觀鎖

    偏向鎖更準確的說是鎖的一種狀態。在這種鎖狀態下,系統中只有一個線程來爭奪這個鎖。線程只要簡單地通過Mark Word中存放的線程ID和自己的ID是否一致就能拿到鎖。下面簡單介紹下偏向鎖獲取和升級的過程。

    還是就着這張圖講吧,會清楚點。

    當系統中還沒有訪問過synchronized代碼時,此時鎖的狀態肯定是“無鎖狀態”,也就是說“是否是偏向鎖”的值是0,“鎖標誌位”的值是01。此時有一個線程1來訪問同步代碼,發現鎖對象的狀態是”無鎖狀態”,那麼操作起來非常簡單了,只需要將“是否偏向鎖”標誌位改成1,再將線程1的線程ID寫入Mark Word即可。

    如果後續系統中一直只有線程1來拿鎖,那麼只要簡單的判斷下線程1的ID和Mark Word中的線程ID,線程1就能非常輕鬆地拿到鎖。但是現實往往不是那麼簡單的,現在假設線程2也要來競爭同步鎖,我們看下情況是怎麼樣的。

    • step1:線程2首先根據“是否是偏向鎖”和“鎖標誌位”的值判斷出當前鎖的狀態是“偏向鎖”狀態,但是Mark Word中的線程ID又不是指向自己(此時線程ID還是指向線程1),所以此時回去判斷線程1還是否存在;
    • step2:假如此時線程1已經不存在了,線程2會將Mark Word中的線程ID指向自己的線程ID,鎖不升級,仍為偏向鎖;
    • step3:假如此時線程1還存在(線程1還沒執行完同步代碼,【不知道這樣理解對不對,姑且先這麼理解吧】),首先暫停線程1,設置鎖標誌位為00,鎖升級為“輕量級鎖”,繼續執行線程1的代碼;線程2通過自旋操作來繼續獲得鎖。

    在JDK6中,偏向鎖是默認啟用的。它提高了單線程訪問同步資源的性能。但試想一下,如果你的同步資源或代碼一直都是多線程訪問的,那麼消除偏向鎖這一步驟對你來說就是多餘的。事實上,消除偏向鎖的開銷還是蠻大的。
    所以在你非常熟悉自己的代碼前提下,大可禁用偏向鎖:

     -XX:-UseBiasedLocking=false

    輕量級鎖

    “輕量級鎖”鎖也是一種鎖的狀態,這種鎖狀態的特點是:當一個線程來競爭鎖失敗時,不會立即進入阻塞狀態,而是會進行一段時間的鎖自旋操作,如果自旋操作拿鎖成功就執行同步代碼,如果經過一段時間的自旋操作還是沒拿到鎖,線程就進入阻塞狀態。

    1. 輕量級鎖加鎖流程
    線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

    2. 輕量級鎖解鎖流程
    輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

    重量級鎖

    因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

    鎖自旋

    自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。

    但是線程自旋是需要消耗CPU的,說白了就是讓CPU在做無用功,線程不能一直佔用CPU自旋做無用功,所以需要設定一個自旋等待的最大時間。如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

    自旋鎖盡可能的減少線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小於線程阻塞掛起操作的消耗!但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是佔用cpu做無用功,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要cup的線程又不能獲取到cpu,造成cpu的浪費。

    JDK7之後,鎖的自旋特性都是由JVM自身控制的,不需要我們手動配置。

    鎖對比

    鎖的優化

    • 減少鎖的時間:不需要同步的代碼不加鎖;
    • 使用讀寫鎖:讀操作加讀鎖,可以併發讀,寫操作使用寫鎖,只能單線程寫;
    • 鎖粗化:假如有一個循環,循環內的操作需要加鎖,我們應該把鎖放到循環外面,否則每次進出循環,都進出一次臨界區,效率是非常差的;

    參考

    • https://blog.csdn.net/lkforce/article/details/81128115
    • 《併發編程藝術》

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

    【其他文章推薦】

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

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

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

    台灣寄大陸海運貨物規則及重量限制?

    大陸寄台灣海運費用試算一覽表