024.掌握Pod-部署MongoDB

一 前期準備

1.1 前置條件


  • 集群部署:Kubernetes集群部署參考003——019。
  • glusterfs-Kubernetes部署:參考《附010.Kubernetes永久存儲之GlusterFS超融合部署》。

1.2 部署規劃


本實驗使用StatefulSet部署MongoDB集群,同時每個MongoDB實例使用glusterfs實現永久存儲。從而部署無單點故障、高可用、可動態擴展的MongoDB集群。

部署架構如下:

二 創建StatefulSet

2.1 創建storageclass存儲類型

  1 [root@k8smaster01 ~]# vi heketi-secret.yaml			#創建用於保存密碼的secret
  2 apiVersion: v1
  3 kind: Secret
  4 metadata:
  5   name: heketi-secret
  6   namespace: heketi
  7 data:
  8   # base64 encoded password. E.g.: echo -n "mypassword" | base64
  9   key: YWRtaW4xMjM=
 10 type: kubernetes.io/glusterfs


  1 [root@k8smaster01 heketi]# kubectl create -f heketi-secret.yaml	#創建heketi
  2 [root@k8smaster01 heketi]# kubectl get secrets -n heketi
  3 NAME                                 TYPE                                  DATA   AGE
  4 default-token-6n746                  kubernetes.io/service-account-token   3      144m
  5 heketi-config-secret                 Opaque                                3      142m
  6 heketi-secret                        kubernetes.io/glusterfs               1      3m1s
  7 heketi-service-account-token-ljlkb   kubernetes.io/service-account-token   3      143m
  8 [root@k8smaster01 ~]# mkdir mongo
  9 [root@k8smaster01 ~]# cd mongo


  1 [root@k8smaster01 heketi]# vi storageclass-fast.yaml
  2 apiVersion: storage.k8s.io/v1
  3 kind: StorageClass
  4 metadata:
  5   name: fast
  6 parameters:
  7   resturl: "http://10.254.82.26:8080"
  8   clusterid: "d96022e907f82045dcc426a752adc47c"
  9   restauthenabled: "true"
 10   restuser: "admin"
 11   secretName: "heketi-secret"
 12   secretNamespace: "default"
 13   volumetype: "replicate:3"
 14 provisioner: kubernetes.io/glusterfs
 15 reclaimPolicy: Delete
  1 [root@k8smaster01 heketi]# kubectl create -f storageclass-fast.yaml
  2 [root@k8smaster01 heketi]# kubectl get storageclasses/fast



2.2 授權ServiceAccount


本實驗2.4步驟需要使用mongo-sidecar的pod來配置管理mongo pod。

由於默認的service account僅僅只能獲取當前Pod自身的相關屬性,無法觀察到其他名稱空間Pod的相關屬性信息。如果想要擴展Pod,或者一個Pod需要用於管理其他Pod或者是其他資源對象,是無法通過自身的名稱空間的serviceaccount進行獲取其他Pod的相關屬性信息的,因此需要進行手動創建一個serviceaccount,並在創建Pod時進行定義。或者直接將默認的serviceaccount進行授權。

  1 [root@uk8s-m-01 mongo]# vi defaultaccout.yaml
  2 ---
  3 apiVersion: rbac.authorization.k8s.io/v1beta1
  4 kind: ClusterRoleBinding
  5 metadata:
  6   name: DDefault-Cluster-Admin
  7 subjects:
  8   - kind: ServiceAccount
  9     # Reference to upper's `metadata.name`
 10     name: default
 11     # Reference to upper's `metadata.namespace`
 12     namespace: default
 13 roleRef:
 14   kind: ClusterRole
 15   name: cluster-admin
 16   apiGroup: rbac.authorization.k8s.io
 17 
 18 [root@uk8s-m-01 mongo]# kubectl apply -f defaultaccout.yaml


2.3 創建headless Service

  1 [root@k8smaster01 mongo]# vi mongo-headless-service.yaml




提示:本實驗直接將headless結合在StatefulSet同一個yaml文件中,參考2.4。

2.4 創建StatefulSet

  1 [root@k8smaster01 mongo]# vi statefulset-mongo.yaml
  2 ---
  3 apiVersion: v1
  4 kind: Service
  5 metadata:
  6   name: mongo
  7   labels:
  8     name: mongo
  9 spec:
 10   ports:
 11   - port: 27017
 12     targetPort: 27017
 13   clusterIP: None
 14   selector:
 15     role: mongo
 16 ---                                  #以上為headless-service
 17 apiVersion: apps/v1beta1
 18 kind: StatefulSet
 19 metadata:
 20   name: mongo
 21 spec:
 22   serviceName: "mongo"
 23   replicas: 3
 24   template:
 25     metadata:
 26       labels:
 27         role: mongo
 28         environment: test
 29     spec:
 30       terminationGracePeriodSeconds: 10
 31       containers:
 32         - name: mongo
 33           image: mongo:3.4             #新版可能不支持smallfiles參數,因此指定為3.4版本
 34           command:
 35             - mongod
 36             - "--replSet"
 37             - rs0
 38             - "--bind_ip"
 39             - 0.0.0.0
 40             - "--smallfiles"           #使用較小的默認文件
 41             - "--noprealloc"           #禁用數據文件預分配
 42           ports:
 43             - containerPort: 27017
 44           volumeMounts:
 45             - name: mongo-persistent-storage
 46               mountPath: /data/db
 47         - name: mongo-sidecar
 48           image: cvallance/mongo-k8s-sidecar
 49           env:
 50             - name: MONGO_SIDECAR_POD_LABELS
 51               value: "role=mongo,environment=test"
 52             - name: KUBERNETES_MONGO_SERVICE_NAME
 53               value: "mongo"
 54   volumeClaimTemplates:
 55   - metadata:
 56       name: mongo-persistent-storage
 57       annotations:
 58         volume.beta.kubernetes.io/storage-class: "fast"
 59     spec:
 60       accessModes: [ "ReadWriteOnce" ]
 61       resources:
 62         requests:
 63           storage: 2Gi



釋義:

  1. 該StatefulSet定義了兩個容器:mingo和mongo-sidecar。mongo是主服務程序,mongo-sidecar是將多個mongo實例進行集群設置的工具。同時mongo-sidecar中設置了如下環境變量:


    • MONGO_SIDECAR_POD_LABELS:設置為mongo容器的標籤,用於sidecar查詢它所要管理的MongoDB集群實例。
    • KUBERNETES_MONGO_SERVICE_NAME:它的值為mongo,表示sidecar將使用mongo這個服務名來完成MongoDB集群的設置。


  1. replicas=3表示MongoDB集群由3個mongo實例組成。
  2. volumeClaimTemplates是StatefulSet最重要的存儲設置。在annotations段設置volume.beta.kubernetes.io/storage-class=”fast”表示使用名為fast的StorageClass自動為每個mongo Pod實例分配後端存儲。
  3. resources.requests.storage=2Gi表示為每個mongo實例都分配2GiB的磁盤空間。




  1 [root@k8smaster01 mongo]# kubectl create -f statefulset-mongo.yaml	#創建mongo


提示:由於國內mongo鏡像可能無法pull,建議通過VPN等方式提前pull鏡像,然後上傳至所有node節點。

  1 [root@VPN ~]# docker pull cvallance/mongo-k8s-sidecar:latest
  2 [root@VPN ~]# docker pull mongo:3.4.4
  3 [root@VPN ~]# docker save -o mongo-k8s-sidecar.tar cvallance/mongo-k8s-sidecar:latest
  4 [root@VPN ~]# docker save -o mongo_3_4_4.tar mongo:3.4.4
  5 [root@k8snode01 ~]# docker load -i mongo-k8s-sidecar.tar
  6 [root@k8snode01 ~]# docker load -i mongo.tar
  7 [root@k8snode01 ~]# docker images



創建異常可通過如下方式刪除,重新創建:

  1 kubectl delete -f statefulset-mongo.yaml
  2 kubectl delete -f mongo-headless-service.yaml
  3 kubectl delete pvc -l role=mongo


三 確認驗證

3.1 查看資源

  1 [root@k8smaster01 mongo]# kubectl get pod -l role=mongo			#查看集群pod
  2 NAME      READY   STATUS    RESTARTS   AGE
  3 mongo-0   2/2     Running   0          9m44s
  4 mongo-1   2/2     Running   0          7m51s
  5 mongo-2   2/2     Running   0          6m1s



StatefulSet會用volumeClaimTemplates中的定義為每個Pod副本都創建一個PVC實例,每個PVC的名稱由StatefulSet定義中volumeClaimTemplates的名稱和Pod副本的名稱組合而成。

  1 [root@k8smaster01 mongo]# kubectl get pvc



  1 [root@k8smaster01 mongo]# kubectl get pods mongo-0 -o yaml | grep -A 3 volumes	#查看掛載


3.2 查看mongo集群


登錄任意一個mongo Pod,在mongo命令行界面用rs.status()命令查看MongoDB集群的狀態,該mongodb集群已由sidecar完成了創建。在集群中包含3個節點 每個節點的名稱都是StatefulSet設置的DNS域名格式的網絡標識名稱:

mongo-0.mongo.default.svc.cluster.local

mongo-1.mongo.default.svc.cluster.local

mongo-2.mongo.default.svc.cluster.local

同時,可以查看每個mongo實例各自的角色(PRIMARY或SECONDARY)。

  1 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  2 ……
  3 rs0:PRIMARY> rs.status()




四 集群常見管理

4.1 MongoDB擴容


運行環境過程中,若3個mongo實例不足以滿足業務的要求,可對mongo集群進行擴容。僅需要通過對StatefulSet進行scale操作,從而實現在mongo集群中自動添加新的mongo節點。

  1 [root@k8smaster01 ~]# kubectl scale statefulset mongo --replicas=4	#擴容為4個
  2 [root@k8smaster01 ~]# kubectl get pod -l role=mongo
  3 NAME      READY   STATUS    RESTARTS   AGE
  4 mongo-0   2/2     Running   0          105m
  5 mongo-1   2/2     Running   0          103m
  6 mongo-2   2/2     Running   0          101m
  7 mongo-3   2/2     Running   0          50m


4.2 查看集群成員

  1 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  2 ……
  3 rs0:PRIMARY> rs.status()
  4 ……



4.3 故障自動恢復


若在系統運行過程中,某個mongo實例或其所在主機發生故障,則StatefulSet將會自動重建該mongo實例,並保證其身份(ID)和使用的數據(PVC) 不變。以下為mongo-0實例發生故障進行模擬,StatefulSet將會自動重建mongo-0實例,併為其掛載之前分配的PVC“mongo-persistent-storage-mongo-0”。新的服務“mongo-0”在重新啟動后,原數據庫中的數據不會丟失,可繼續使用。

  1 [root@k8smaster01 ~]# kubectl get pvc
  2 [root@k8smaster01 ~]# kubectl delete pod mongo-0
  3 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  4 ……
  5 rs0:PRIMARY> rs.status()
  6 ……





提示:進入某個實例查看mongo集群的狀態,mongo-0發生故障前在集群中的角色為PRIMARY,在其脫離集群后,mongo集群會自動選出一個SECONDARY節點提升為PRIMARY節點(本例中為mongo-2)。重啟后的mongo-0則會成為一個新的SECONDARY節點。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

從壹開始 [ Design Pattern ] 之二 ║ 單例模式 與 Singleton

前言

這一篇來源我的公眾號,如果你沒看過,正好直接看看,如果看過了也可以再看看,我稍微修改了一些內容,今天講解的內容如下

 

 

 

 

 

 

 

一、什麼是單例模式

 

【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是屬於創建類型的一種常用的軟件設計模式。通過單例模式的方法創建的類在當前進程中只有一個實例(根據需要,也有可能一個線程中屬於單例,如:僅線程上下文內使用同一個實例)。

1、單例類只能有一個實例。

2、單例類必須自己創建自己的唯一實例。

3、單例類必須給所有其他對象提供這一實例。

 

那咱們大概知道了,其實說白了,就是我們整個項目周期內,只會有一個實例,當項目停止的時候,實例銷毀,當重新啟動的時候,我們的實例又會產品。

上文中說到了一個名詞【創建類型】的設計模式,那什麼是創建類型的設計模式呢?

創建型(Creational)模式:負責對象創建,我們使用這個模式,就是為了創建我們需要的對象實例的。

 

那除了創建型還有其他兩種類型的模式:

結構型(Structural)模式:處理類與對象間的組合

行為型(Behavioral)模式:類與對象交互中的職責分

這兩種設計模式,以後會慢慢說到,這裏先按下不表。

咱們就重點從0開始分析分析如何創建一個單例模式的對象實例。

 

二、如何創建單例模式

 

實現單例模式有很多方法:從“懶漢式”到“餓漢式”,最後“雙檢鎖”模式,這裏咱們就慢慢的,從一步一步的開始講解如何創建單例。

 

1、正常的思考邏輯順序

 

既然要創建單一的實例,那我們首先需要學會如何去創建一個實例,這個很簡單,相信每個人都會創建實例,就比如說這樣的:

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    public WeatherForecast()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}


 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = new WeatherForecast();
     return weather;
 }

 

我們每次訪問的時候,時間都是會變化,所以我們的實例也是一直在創建,在變化:

 

 

相信每個人都能看到這個代碼是什麼意思,不多說,直接往下走,我們知道,單例模式的核心目的就是:

必須保證這個實例在整個系統的運行周期內是唯一的,這樣可以保證中間不會出現問題。

 

那好,我們改進改進,不是說要唯一一個么,好說!我直接返回不就行了:

 

 /// <summary>
 /// 定義一個天氣類
 /// </summary>
 public class WeatherForecast
 {
     // 定義一個靜態變量來保存類的唯一實例
     private static WeatherForecast uniqueInstance;

     // 定義私有構造函數,使外界不能創建該類實例
     private WeatherForecast()
     {
         Date = DateTime.Now;
     }
     /// <summary>
     /// 靜態方法,來返回唯一實例
     /// 如果存在,則返回
     /// </summary>
     /// <returns></returns>
     public static WeatherForecast GetInstance()
     {
         // 如果類的實例不存在則創建,否則直接返回
         // 其實嚴格意義上來說,這個不屬於【單例】
         if (uniqueInstance == null)
         {
             uniqueInstance = new WeatherForecast();
         }
         return uniqueInstance;
     }
     public DateTime Date { get; set; }public int TemperatureC { get; set; }
     public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
     public string Summary { get; set; }
 }

 

 

然後我們修改一下調用方法,因為我們的默認構造函數已經私有化了,不允許再創建實例了,所以我們直接這麼調用:

[HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = WeatherForecast.GetInstance();
     return weather;
 }

 

最後來看看效果:

 

 

這個時候,我們可以看到,時間已經不發生變化了,也就是說我們的實例是唯一的了,大功告成!是不是很開心!

 

但是,別著急,問題來了,我們目前是單線程的,所以只有一個,那如果多線程呢,如果多個線程同時訪問,會不會也會正常呢?

這裏我們做一個測試,我們在項目啟動的時候,用多線程去調用:

 

 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多線程去調用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             WeatherForecast.GetInstance();
         })
         );
         th.Start(i);
     }
     return null;
 }

 

然後我們看看效果是怎樣的,按照我們的思路,應該是只會走一遍構造函數,其實不是:

 

 

 

 

 

 

3個線程在第一次訪問GetInstance方法時,同時判斷(uniqueInstance ==null)這個條件時都返回真,然後都去創建了實例,這個肯定是不對的。那怎麼辦呢,只要讓GetInstance方法只運行一個線程運行就好了,我們可以加一個鎖來控制他,代碼如下:

public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        lock (locker)
        {
            // 如果類的實例不存在則創建,否則直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new WeatherForecast();
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}

 

這個時候,我們再併發測試,發現已經都一樣了,這樣就達到了我們想要的效果,但是這樣真的是最完美的么,其實不是的,因為我們加鎖,只是第一次判斷是否為空,如果創建好了以後,以後就不用去管這個 lock 鎖了,我們只關心的是 uniqueInstance 是否為空,那我們再完善一下:

 

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                // 如果類的實例不存在則創建,否則直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new WeatherForecast();
                }
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}

 

這樣才最終的完美實現我們的單例模式!搞定。

 

2、幽靈事件:指令重排

當然,如果你看完了上邊的那四步已經可以出師了,平時我們就是這麼使用的,也是這麼想的,但是真的就是萬無一失么,有一個 JAVA 的朋友提出了這個問題,C# 中我沒有聽說過,是我孤陋寡聞了么:

單例模式的幽靈事件,時令重排會偶爾導致單例模式失效。

 

是不是聽起來感覺很高大上,而不知所云,沒關係,咱們平時用不到,但是可以了解了解:

為何要指令重排?       

指令重排是指的 volatile,現在的CPU一般採用流水線來執行指令。一個指令的執行被分成:取指、譯碼、訪存、執行、寫回、等若干個階段。然後,多條指令可以同時存在於流水線中,同時被執行。
指令流水線並不是串行的,並不會因為一個耗時很長的指令在“執行”階段呆很長時間,而導致後續的指令都卡在“執行”之前的階段上。
相反,流水線是并行的,多個指令可以同時處於同一個階段,只要CPU內部相應的處理部件未被佔滿即可。比如說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於“執行”階段, 而兩條加法指令在“執行”階段就只能串行工作。
相比於串行+阻塞的方式,流水線像這樣并行的工作,效率是非常高的。

然而,這樣一來,亂序可能就產生了。比如一條加法指令原本出現在一條除法指令的後面,但是由於除法的執行時間很長,在它執行完之前,加法可能先執行完了。再比如兩條訪存指令,可能由於第二條指令命中了cache而導致它先於第一條指令完成。
一般情況下,指令亂序並不是CPU在執行指令之前刻意去調整順序。CPU總是順序的去內存裏面取指令,然後將其順序的放入指令流水線。但是指令執行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終亂序執行完成。這就是所謂的“順序流入,亂序流出”。

 

這個是從網上摘錄的,大概意思看看就行,理解雙檢鎖失效原因有兩個重點

1、編譯器的寫操作重排問題.
例 : B b = new B();

上面這一句並不是原子性的操作,一部分是new一個B對象,一部分是將new出來的對象賦值給b.

直覺來說我們可能認為是先構造對象再賦值.但是很遺憾,這個順序並不是固定的.再編譯器的重排作用下,可能會出現先賦值再構造對象的情況.

2、結合上下文,結合使用情景.

理解了1中的寫操作重排以後,我卡住了一下.因為我真不知道這種重排到底會帶來什麼影響.實際上是因為我看代碼看的不夠仔細,沒有意識到使用場景.雙檢鎖的一種常見使用場景就是在單例模式下初始化一個單例並返回,然後調用初始化方法的方法體內使用初始化完成的單例對象.

 

三、Singleton = 單例 ?

 上邊我們說了很多,也介紹了很多單例的原理和步驟,那這裏問題來了,我們在學習依賴注入的時候,用到的 Singleton 的單例注入,是不是和上邊說的一回事兒呢,這裏咱們直接多多線程測試一下就行:

 

/// <summary>
/// 定義一個心情類
/// </summary>
public class Feeling
{
    public Feeling()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
}


 // 單例注入
 services.AddSingleton<Feeling>();


[HttpGet]
public WeatherForecast Get()
{

    // 多線程去調用
    for (int i = 0; i < 3; i++)
    {
        var th = new Thread(
        new ParameterizedThreadStart((state) =>
        {
            //WeatherForecast.GetInstance();
            
            // 此刻的心情
            Feeling feeling = new Feeling();
            Console.WriteLine(feeling.Date);
        })
        );
        th.Start(i);
    }
    return null;
}

 

測試的結果,情理之中,也是意料之外:

 

 

竟然和我們上邊說的是一樣的, 
Singleton是一種懶漢模式 的單例, 因為結論可以看出,有時候我們使用單例模式,並不是寫一個 Sigleton 就能滿足的。    

四、單例模式的優缺點

 

        【優】、單例模式的優點:

             (1)、保證唯一性:防止其他對象實例化,保證實例的唯一性;

             (2)、全局性:定義好數據后,可以再整個項目種的任何地方使用當前實例,以及數據;

        【劣】、單例模式的缺點: 

             (1)、內存常駐:因為單例的生命周期最長,存在整個開發系統內,如果一直添加數據,或者是常駐的話,會造成一定的內存消耗。

 

以下內容來自百度百科:

優點

一、實例控制 單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。
二、靈活性 因為類控制了實例化過程,所以類可以靈活更改實例化過程。  

缺點

一、開銷 雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
二、可能的開發混淆 使用單例對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用
new關鍵字實例化對象。因為可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。
三、對象生存期 不能解決刪除單個對象的問題。在提供內存管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致實例被取消分配,因為它包含對該實例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實例,但這樣會導致單例類中出現懸浮引用。

 

五、示例代碼

 

 

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

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

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

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

圖文詳解基於角色的權限控制模型RBAC

我們開發一個系統,必然面臨權限控制的問題,即不同的用戶具有不同的訪問、操作、數據權限。形成理論的權限控制模型有:自主訪問控制(DAC: Discretionary Access Control)、強制訪問控制(MAC: Mandatory Access Control)、基於屬性的權限驗證(ABAC: Attribute-Based Access Control)等。最常被開發者使用也是相對易用、通用的就是RBAC權限模型(Role-Based Access Control),本文就將向大家介紹該權限模型。

一、RBAC權限模型簡介

RBAC權限模型(Role-Based Access Control)即:基於角色的權限控制。模型中有幾個關鍵的術語:

  • 用戶:系統接口及訪問的操作者
  • 權限:能夠訪問某接口或者做某操作的授權資格
  • 角色:具有一類相同操作權限的用戶的總稱

RBAC權限模型核心授權邏輯如下:

  • 某用戶是什麼角色?
  • 某角色具有什麼權限?
  • 通過角色的權限推導用戶的權限

二、RBAC的演化進程

2.1.用戶與權限直接關聯

想到權限控制,人們最先想到的一定是用戶與權限直接關聯的模式,簡單地說就是:某個用戶具有某些權限。如圖:

  • 張三具有創建用戶和刪除用戶的權限,所以他可能系統維護人員
  • 李四具有產品記錄管理和銷售記錄管理權限,所以他可能是一個業務銷售人員

這種模型能夠清晰的表達用戶與權限之間的關係,足夠簡單。但同時也存在問題:

  • 現在用戶是張三、李四,以後隨着人員增加,每一個用戶都需要重新授權
  • 或者張三、李四離職,需要針對每一個用戶進行多種權限的回收

2.2.一個用戶擁有一個角色

在實際的團體業務中,都可以將用戶分類。比如對於薪水管理系統,通常按照級別分類:經理、高級工程師、中級工程師、初級工程師。也就是按照一定的角色分類,通常具有同一角色的用戶具有相同的權限。這樣改變之後,就可以將針對用戶賦權轉換為針對角色賦權。

  • 一個用戶有一個角色
  • 一個角色有多個操作(菜單)權限
  • 一個操作權限可以屬於多個角色

我們可以用下圖中的數據庫設計模型,描述這樣的關係。

2.3 一個用戶一個或多個角色

但是在實際的應用系統中,一個用戶一個角色遠遠滿足不了需求。如果我們希望一個用戶既擔任銷售角色、又暫時擔任副總角色。該怎麼做呢?為了增加系統設計的適用性,我們通常設計:

  • 一個用戶有一個或多個角色
  • 一個角色包含多個用戶
  • 一個角色有多種權限
  • 一個權限屬於多個角色

我們可以用下圖中的數據庫設計模型,描述這樣的關係。

二、頁面訪問權限與操作權限

  • 頁面訪問權限: 所有系統都是由一個個的頁面組成,頁面再組成模塊,用戶是否能看到這個頁面的菜單、是否能進入這個頁面就稱為頁面訪問權限。
  • 操作權限: 用戶在操作系統中的任何動作、交互都需要有操作權限,如增刪改查等。比如:某個按鈕,某個超鏈接用戶是否可以點擊,是否應該看見的權限。

為了適應這種需求,我們可以把頁面資源(菜單)和操作資源(按鈕)分表存放,如上圖。也可以把二者放到一個表裡面存放,用一個字段進行標誌區分。

三、數據權限

數據權限比較好理解,就是某個用戶能夠訪問和操作哪些數據。

  • 通常來說,數據權限由用戶所屬的組織來確定。比如:生產一部只能看自己部門的生產數據,生產二部只能看自己部門的生產數據;銷售部門只能看銷售數據,不能看財務部門的數據。而公司的總經理可以看所有的數據。
  • 在實際的業務系統中,數據權限往往更加複雜。非常有可能銷售部門可以看生產部門的數據,以確定銷售策略、安排計劃等。

所以為了面對複雜的需求,數據權限的控制通常是由程序員書寫個性化的SQL來限制數據範圍的,而不是交給權限模型或者Spring Security或shiro來控制。當然也可以從權限模型或者權限框架的角度去解決這個問題,但適用性有限。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

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

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

使用Amazon EMR和Apache Hudi在S3上插入,更新,刪除數據

將數據存儲在Amazon S3中可帶來很多好處,包括規模、可靠性、成本效率等方面。最重要的是,你可以利用Amazon EMR中的Apache Spark,Hive和Presto之類的開源工具來處理和分析數據。 儘管這些工具功能強大,但是在處理需要進行增量數據處理以及記錄級別插入,更新和刪除場景時,仍然非常具有挑戰。

與客戶交談時,我們發現有些場景需要處理對單條記錄的增量更新,例如:

  • 遵守數據隱私法規,在該法規中,用戶選擇忘記或更改應用程序對數據使用方式的協議。
  • 使用流數據,當你必須要處理特定的數據插入和更新事件時。
  • 實現變更數據捕獲(CDC)架構來跟蹤和提取企業數據倉庫或運營數據存儲中的數據庫變更日誌。
  • 恢復遲到的數據,或分析特定時間點的數據。

從今天開始,EMR 5.28.0版包含Apache Hudi(孵化中),因此你不再需要構建自定義解決方案來執行記錄級別的插入,更新和刪除操作。Hudi是Uber於2016年開始開發,以解決攝取和ETL管道效率低下的問題。最近幾個月,EMR團隊與Apache Hudi社區緊密合作,提供了一些補丁,包括將Hudi更新為Spark 2.4.4,支持Spark Avro,增加了對AWS Glue Data Catalog的支持,以及多個缺陷修復。

使用Hudi,即可以在S3上執行記錄級別的插入,更新和刪除,從而使你能夠遵守數據隱私法律、消費實時流、捕獲更新的數據、恢復遲到的數據和以開放的、供應商無關的格式跟蹤歷史記錄和回滾。 創建數據集和表,然後Hudi管理底層數據格式。Hudi使用Apache Parquet和Apache Avro進行數據存儲,並內置集成Spark,Hive和Presto,使你能夠使用與現在所使用的相同工具來查詢Hudi數據集,並且幾乎實時地訪問新數據。

啟動EMR群集時,只要選擇以下組件之一(Hive,Spark,Presto),就可以自動安裝和配置Hudi的庫和工具。你可以使用Spark創建新的Hudi數據集,以及插入,更新和刪除數據。每個Hudi數據集都會在集群的已配置元存儲庫(包括AWS Glue Data Catalog)中進行註冊,並显示為可以通過Spark,Hive和Presto查詢的表。

Hudi支持兩種存儲類型,這些存儲類型定義了如何寫入,索引和從S3讀取數據:

  • 寫時複製(Copy On Write)– 數據以列格式(Parquet)存儲,並且在寫入時更新數據數據會創建新版本文件。此存儲類型最適合用於讀取繁重的工作負載,因為數據集的最新版本在高效的列式文件中始終可用。

  • 讀時合併(Merge On Read)– 將組合列(Parquet)格式和基於行(Avro)格式來存儲數據; 更新記錄至基於行的增量文件中,並在以後進行壓縮,以創建列式文件的新版本。 此存儲類型最適合於繁重的寫工作負載,因為新提交(commit)會以增量文件格式快速寫入,但是要讀取數據集,則需要將壓縮的列文件與增量文件合併。

下面讓我們快速預覽下如何在EMR集群中設置和使用Hudi數據集。

結合Apache Hudi與Amazon EMR

從EMR控制台開始創建集群。在高級選項中,選擇EMR版本5.28.0(第一個包括Hudi的版本)和以下應用程序:Spark,Hive和Tez。在硬件選項中,添加了3個任務節點,以確保有足夠的能力運行Spark和Hive。

群集就緒后,使用在安全性選項中選擇的密鑰對,通過SSH進入主節點並訪問Spark Shell。 使用以下命令來啟動Spark Shell以將其與Hudi一起使用:

$ spark-shell --conf "spark.serializer=org.apache.spark.serializer.KryoSerializer"
              --conf "spark.sql.hive.convertMetastoreParquet=false"
              --jars /usr/lib/hudi/hudi-spark-bundle.jar,/usr/lib/spark/external/lib/spark-avro.jar

使用以下Scala代碼將一些示例ELB日誌導入寫時複製存儲類型的Hudi數據集中:

import org.apache.spark.sql.SaveMode
import org.apache.spark.sql.functions._
import org.apache.hudi.DataSourceWriteOptions
import org.apache.hudi.config.HoodieWriteConfig
import org.apache.hudi.hive.MultiPartKeysValueExtractor

//Set up various input values as variables
val inputDataPath = "s3://athena-examples-us-west-2/elb/parquet/year=2015/month=1/day=1/"
val hudiTableName = "elb_logs_hudi_cow"
val hudiTablePath = "s3://MY-BUCKET/PATH/" + hudiTableName

// Set up our Hudi Data Source Options
val hudiOptions = Map[String,String](
    DataSourceWriteOptions.RECORDKEY_FIELD_OPT_KEY -> "request_ip",
    DataSourceWriteOptions.PARTITIONPATH_FIELD_OPT_KEY -> "request_verb", 
    HoodieWriteConfig.TABLE_NAME -> hudiTableName, 
    DataSourceWriteOptions.OPERATION_OPT_KEY ->
        DataSourceWriteOptions.INSERT_OPERATION_OPT_VAL, 
    DataSourceWriteOptions.PRECOMBINE_FIELD_OPT_KEY -> "request_timestamp", 
    DataSourceWriteOptions.HIVE_SYNC_ENABLED_OPT_KEY -> "true", 
    DataSourceWriteOptions.HIVE_TABLE_OPT_KEY -> hudiTableName, 
    DataSourceWriteOptions.HIVE_PARTITION_FIELDS_OPT_KEY -> "request_verb", 
    DataSourceWriteOptions.HIVE_ASSUME_DATE_PARTITION_OPT_KEY -> "false", 
    DataSourceWriteOptions.HIVE_PARTITION_EXTRACTOR_CLASS_OPT_KEY ->
        classOf[MultiPartKeysValueExtractor].getName)

// Read data from S3 and create a DataFrame with Partition and Record Key
val inputDF = spark.read.format("parquet").load(inputDataPath)

// Write data into the Hudi dataset
inputDF.write
       .format("org.apache.hudi")
       .options(hudiOptions)
       .mode(SaveMode.Overwrite)
       .save(hudiTablePath)

在Spark Shell中,現在就可以計算Hudi數據集中的記錄:

scala> inputDF2.count()
res1: Long = 10491958

在選項(options)中,使用了與為集群中的Hive Metastore集成,以便在默認數據庫(default)中創建表。 通過這種方式,我可以使用Hive查詢Hudi數據集中的數據:

hive> use default;
hive> select count(*) from elb_logs_hudi_cow;
...
OK
10491958

現在可以更新或刪除數據集中的單條記錄。 在Spark Shell中,設置了一些用來查詢更新記錄的變量,並準備用來選擇要更改的列的值的SQL語句:

val requestIpToUpdate = "243.80.62.181"
val sqlStatement = s"SELECT elb_name FROM elb_logs_hudi_cow WHERE request_ip = '$requestIpToUpdate'"

執行SQL語句以查看列的當前值:

scala> spark.sql(sqlStatement).show()
+------------+                                                                  
|    elb_name|
+------------+
|elb_demo_003|
+------------+

然後,選擇並更新記錄:

// Create a DataFrame with a single record and update column value
val updateDF = inputDF.filter(col("request_ip") === requestIpToUpdate)
                      .withColumn("elb_name", lit("elb_demo_001"))

現在用一種類似於創建Hudi數據集的語法來更新它。 但是這次寫入的DataFrame僅包含一條記錄:

// Write the DataFrame as an update to existing Hudi dataset
updateDF.write
        .format("org.apache.hudi")
        .options(hudiOptions)
        .option(DataSourceWriteOptions.OPERATION_OPT_KEY,
                DataSourceWriteOptions.UPSERT_OPERATION_OPT_VAL)
        .mode(SaveMode.Append)
        .save(hudiTablePath)

在Spark Shell中,檢查更新的結果:

scala> spark.sql(sqlStatement).show()
+------------+                                                                  
|    elb_name|
+------------+
|elb_demo_001|
+------------+

現在想刪除相同的記錄。要刪除它,可在寫選項中傳入了EmptyHoodieRecordPayload有效負載:

// Write the DataFrame with an EmptyHoodieRecordPayload for deleting a record
updateDF.write
        .format("org.apache.hudi")
        .options(hudiOptions)
        .option(DataSourceWriteOptions.OPERATION_OPT_KEY,
                DataSourceWriteOptions.UPSERT_OPERATION_OPT_VAL)
        .option(DataSourceWriteOptions.PAYLOAD_CLASS_OPT_KEY,
                "org.apache.hudi.EmptyHoodieRecordPayload")
        .mode(SaveMode.Append)
        .save(hudiTablePath)

在Spark Shell中,可以看到該記錄不再可用:

scala> spark.sql(sqlStatement).show()
+--------+                                                                      
|elb_name|
+--------+
+--------+

Hudi是如何管理所有的更新和刪除? 我們可以通過Hudi命令行界面(CLI)連接到數據集,便可以看到這些更改被解釋為提交(commits):

可以看到,此數據集是寫時複製數據集,這意味着每次對記錄進行更新時,包含該記錄的文件將被重寫以包含更新后的值。 你可以查看每次提交(commit)寫入了多少記錄。表格的底行描述了數據集的初始創建,上方是單條記錄更新,頂部是單條記錄刪除。

使用Hudi,你可以回滾到每個提交。 例如,可以使用以下方法回滾刪除操作:

hudi:elb_logs_hudi_cow->commit rollback --commit 20191104121031

在Spark Shell中,記錄現在回退到更新之後的位置:

scala> spark.sql(sqlStatement).show()
+------------+                                                                  
|    elb_name|
+------------+
|elb_demo_001|
+------------+

寫入時複製是默認存儲類型。 通過將其添加到我們的hudiOptions中,我們可以重複上述步驟來創建和更新讀時合併數據集類型:

DataSourceWriteOptions.STORAGE_TYPE_OPT_KEY -> "MERGE_ON_READ"

如果更新讀時合併數據集並使用Hudi CLI查看提交(commit)時,則可以看到讀時合併寫時複製相比有何不同。使用讀時合併,你僅寫入更新的行,而不像寫時複製一樣寫入整個文件。這就是為什麼讀時合併對於需要更多寫入或使用較少讀取次數更新或刪除繁重工作負載的用例很有幫助的原因。增量提交作為Avro記錄(基於行的存儲)寫入磁盤,而壓縮數據作為Parquet文件(列存儲)寫入。為避免創建過多的增量文件,Hudi會自動壓縮數據集,以便使得讀取盡可能地高效。

創建讀時合併數據集時,將創建兩個Hive表:

  • 第一個表的名稱與數據集的名稱相同。
  • 第二個表的名稱後面附加了字符_rt; _rt後綴表示實時。

查詢時,第一個表返回已壓縮的數據,並不會显示最新的增量提交。使用此表可提供最佳性能,但會忽略最新數據。查詢實時表會將壓縮的數據與讀取時的增量提交合併,因此該數據集稱為讀時合併。這將導致可以使用最新數據,但會導致性能開銷,並且性能不如查詢壓縮數據。這樣,數據工程師和分析人員可以靈活地在性能和數據新鮮度之間進行選擇。

已可用

EMR 5.28.0的所有地區現在都可以使用此新功能。將Hudi與EMR結合使用無需額外費用。你可以在EMR文檔中了解更多有關Hudi的信息。 這個新工具可以簡化你在S3中處理,更新和刪除數據的方式。也讓我們知道你打算將其用於哪些場景!

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

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

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

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

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

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

SpringBoot Application深入學習

本節主要介紹SpringBoot Application類相關源碼的深入學習。

主要包括:

  1. SpringBoot應用自定義啟動配置
  2. SpringBoot應用生命周期,以及在生命周期各個階段自定義配置。

本節採用SpringBoot 2.1.10.RELASE,對應示例源碼在:

SpringBoot應用啟動過程:

SpringApplication application = new SpringApplication(DemoApplication.class);
application.run(args);

一、Application類自定義啟動配置

創建SpringApplication對象后,在調用run方法之前,我們可以使用SpringApplication對象來添加一些配置,比如禁用banner、設置應用類型、設置配置文件(profile)

舉例:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(DemoApplication.class);
        // 設置banner禁用
        application.setBannerMode(Banner.Mode.OFF);
        // 將application-test文件啟用為profile
        application.setAdditionalProfiles("test");
        // 設置應用類型為NONE,即啟動完成后自動關閉
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

}

​ 也可以使用SpringApplicationBuilder類來創建SpringApplication對象,builder類提供了鏈式調用的API,更方便調用,增強了可讀性。

        new SpringApplicationBuilder(YqManageCenterApplication.class)
                .bannerMode(Banner.Mode.OFF)
                .profiles("test")
                .web(WebApplicationType.NONE)
                .run(args);

二、application生命周期

SpringApplication的生命周期主要包括:

  1. 準備階段:主要包括加載配置、設置主bean源、推斷應用類型(三種)、創建和設置SpringBootInitializer、創建和設置Application監聽器、推斷主入口類
  2. 運行階段:開啟時間監聽、加載運行監聽器、創建Environment、打印banner、創建和裝載context、廣播應用已啟動、廣播應用運行中

我們先來看一下源碼的分析:

SpringBootApplication構造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        
        // 設置默認配置
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.isCustomEnvironment = false;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        // 設置主bean源
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        // 推斷和設置應用類型(三種)
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 創建和設置SpringBootInitializer
  this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 創建和設置SpringBoot監聽器
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        // 推斷和設置主入口類
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

SpringApplication.run方法源碼:

public ConfigurableApplicationContext run(String... args) {
        // 開啟時間監聽
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
    
        // 加載Spring應用運行監聽器(SpringApplicationRunListenter)
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            // 創建environment(包括PropertySources和Profiles)
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            
            // 打印banner
            Banner printedBanner = this.printBanner(environment);
            
            // 創建context(不同的應用類型對應不同的上下文)
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            // 裝載context(其中還初始化了IOC容器)
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            // 調用applicationContext.refresh
            this.refreshContext(context);
            // 空方法
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop(); // 關閉時間監聽;這樣可以計算出完整的啟動時間
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            // 廣播SpringBoot應用已啟動,會調用所有SpringBootApplicationRunListener里的started方法
            listeners.started(context);
            
            // 遍歷所有ApplicationRunner和CommadnLineRunner的實現類,執行其run方法
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            // 廣播SpringBoot應用運行中,會調用所有SpringBootApplicationRunListener里的running方法
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            // run出現異常時,處理異常;會調用報錯的listener里的failed方法,廣播應用啟動失敗,將異常擴散出去
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

三、application生命周期自定義配置

在SpringApplication的生命周期中,我們還可以添加一些自定義的配置。

下面的配置,主要是通過實現Spring提供的接口,然後在resources下新建META-INF/spring.factories文件,在裏面添加這個類而實現引入的。

準備階段,可以添加如下自定義配置:

3.1 自定義ApplicationContextInitializer的實現類

@Order(100)
public class MyInitializer implements ApplicationContextInitializer {

@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
    System.out.println("自定義的應用上下文初始化器:" + configurableApplicationContext.toString());
}
}

再定義一個My2Initializer,設置@Order(101)

然後在spring.factories文件里如下配置:

# initializers
org.springframework.context.ApplicationContextInitializer=\
  com.example.applicationdemo.MyInitializer,\
  com.example.applicationdemo.My2Initializer

啟動項目:

3.2 自定義ApplicationListener的實現類

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}![file](https://img2018.cnblogs.com/blog/1860493/201911/1860493-20191125130012982-1676057906.png)

即監聽ApplicationEvents類的ApplicationListener接口的實現類。

首先查看有多少種ApplicationEvents:

裏面還可以進行拆分。

我們這裏設置兩個ApplicationListener,都用於監聽ApplicationEnvironmentPreparedEvent

@Order(200)
public class MyApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
        System.out.println("MyApplicationListener: 應用環境準備完畢" + applicationEnvironmentPreparedEvent.toString());
    }
}

在spring.factories中加入applicationListener的配置:

# application-listeners
org.springframework.context.ApplicationListener=\
  com.example.applicationdemo.MyApplicationListener,\
  com.example.applicationdemo.MyApplicationListener2

啟動階段,可以添加如下自定義配置:

3.3 自定義SpringBootRunListener的實現類

監聽整個SpringBoot應用生命周期

public interface SpringApplicationRunListener {
    // 應用啟動
    void starting();

    // 應用ConfigurableEnvironment準備完畢,此刻可以將其調整
    void environmentPrepared(ConfigurableEnvironment environment);

    // 上下文準備完畢
    void contextPrepared(ConfigurableApplicationContext context);

    // 上下文裝載完畢
    void contextLoaded(ConfigurableApplicationContext context);

    // 啟動完成(Beans已經加載到容器中)
    void started(ConfigurableApplicationContext context);

    // 應用運行中
    void running(ConfigurableApplicationContext context);

    // 應用運行失敗
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

我們可以自定義SpringApplicationRunListener的實現類,通過重寫以上方法來定義自己的listener。

比如:

public class MyRunListener implements SpringApplicationRunListener {

    // 注意要加上這個構造器,兩個參數都不能少,否則啟動會報錯,報錯的詳情可以看這個類的最下面
    public MyRunListener(SpringApplication springApplication, String[] args) {

    }

    @Override
    public void starting() {
        System.out.println("MyRunListener: 程序開始啟動");
    }

    // 其他方法省略,不做修改
}

然後在spring.factories文件中添加這個類:

org.springframework.boot.SpringApplicationRunListener=\
  com.example.applicationdemo.MyRunListener

啟動:

3.4 自定義ApplicationRunner或CommandLineRunner

application的run方法中,有這樣一行:

this.callRunners(context, applicationArguments);

仔細分析源碼,發現這一句的作用是:SpringBoot應用啟動過程中,會遍歷所有的ApplicationRunner和CommandLineRunner,執行其run方法。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();

        while(var4.hasNext()) {
            Object runner = var4.next();
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }

            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }

    }
@FunctionalInterface
public interface CommandLineRunner {
    void run(String... args) throws Exception;
}
@FunctionalInterface
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}

分別定義一個實現類,添加@Component,這兩個實現類不需要在spring.factories中配置

好了,關於這些自定義配置的具體使用,後續會繼續進行介紹,請持續關注!感謝!

具體示例代碼請去查看。

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

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

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

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

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

你必須知道的容器日誌 (2) 開源日誌管理方案 ELK/EFK

本篇已加入《》,可以點擊查看更多容器化技術相關係列文章。上一篇《》中介紹了Docker自帶的logs子命令以及其Logging driver,本篇將會介紹一個流行的開源日誌管理方案ELK。

一、關於ELK

1.1 ELK簡介

  ELK 是Elastic公司提供的一套完整的日誌收集以及展示的解決方案,是三個產品的首字母縮寫,分別是ElasticSearchLogstashKibana

  • Elasticsearch是實時全文搜索和分析引擎,提供搜集、分析、存儲數據三大功能
  • Logstash是一個用來搜集、分析、過濾日誌的工具
  • Kibana是一個基於Web的圖形界面,用於搜索、分析和可視化存儲在 Elasticsearch指標中的日誌數據   

1.2 ELK日誌處理流程

   上圖展示了在Docker環境下,一個典型的ELK方案下的日誌收集處理流程:

  • Logstash從各個Docker容器中提取日誌信息
  • Logstash將日誌轉發到ElasticSearch進行索引和保存
  • Kibana負責分析和可視化日誌信息

  由於Logstash在數據收集上並不出色,而且作為Agent,其性能並不達標。基於此,Elastic發布了beats系列輕量級採集組件。

  這裏我們要實踐的Beat組件是Filebeat,Filebeat是構建於beats之上的,應用於日誌收集場景的實現,用來替代 Logstash Forwarder 的下一代 Logstash 收集器,是為了更快速穩定輕量低耗地進行收集工作,它可以很方便地與 Logstash 還有直接與 Elasticsearch 進行對接。

  本次實驗直接使用Filebeat作為Agent,它會收集我們在第一篇《》中介紹的json-file的log文件中的記錄變動,並直接將日誌發給ElasticSearch進行索引和保存,其處理流程變為下圖,你也可以認為它可以稱作 EFK。

二、ELK套件的安裝

  本次實驗我們採用Docker方式部署一個最小規模的ELK運行環境,當然,實際環境中我們或許需要考慮高可用和負載均衡。

  首先拉取一下sebp/elk這個集成鏡像,這裏選擇的tag版本是640(最新版本已經是7XX了):

docker pull sebp/elk:640

  注:由於其包含了整個ELK方案,所以需要耐心等待一會。

  通過以下命令使用sebp/elk這個集成鏡像啟動運行ELK:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
    sebp/elk:640

  運行完成之後就可以先訪問一下 http://[Your-HostIP]:5601 看看Kibana的效果:  

  Kibana管理界面

Kibana Index Patterns界面

  當然,目前沒有任何可以显示的ES的索引和數據,再訪問一下http://[Your-HostIP]:9200 看看ElasticSearch的API接口是否可用:

ElasticSearch API

  Note:如果啟動過程中發現一些錯誤,導致ELK容器無法啟動,可以參考《》及《》一文。如果你的主機內存低於4G,建議增加配置設置ES內存使用大小,以免啟動不了。例如下面增加的配置,限制ES內存使用最大為1G:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
  -e ES_MIN_MEM=512m \ -e ES_MAX_MEM=1024m \ sebp/elk:640

三、Filebeat配置

3.1 安裝Filebeat

  這裏我們通過rpm的方式下載Filebeat,注意這裏下載和我們ELK對應的版本(ELK是6.4.0,這裏也是下載6.4.0,避免出現錯誤):

wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.4.0-x86_64.rpm
rpm -ivh filebeat-6.4.0-x86_64.rpm

3.2 配置Filebeat  

   這裏我們需要告訴Filebeat要監控哪些日誌文件 及 將日誌發送到哪裡去,因此我們需要修改一下Filebeat的配置:

cd /etc/filebeat
vim filebeat.yml

  要修改的內容為:

  (1)監控哪些日誌?

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/lib/docker/containers/*/*.log - /var/log/syslog

  這裏指定paths:/var/lib/docker/containers/*/*.log,另外需要注意的是將 enabled 設為 true。

  (2)將日誌發到哪裡?

#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
  # Array of hosts to connect to.
  hosts: ["192.168.16.190:9200"]

  # Optional protocol and basic auth credentials.
  #protocol: "https"
  #username: "elastic"
  #password: "changeme"

  這裏指定直接發送到ElasticSearch,配置一下ES的接口地址即可。

  Note:如果要發到Logstash,請使用後面這段配置,將其取消註釋進行相關配置即可:

#----------------------------- Logstash output --------------------------------
#output.logstash:
  # The Logstash hosts
  #hosts: ["localhost:5044"]

  # Optional SSL. By default is off.
  # List of root certificates for HTTPS server verifications
  #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]

  # Certificate for SSL client authentication
  #ssl.certificate: "/etc/pki/client/cert.pem"

  # Client Certificate Key
  #ssl.key: "/etc/pki/client/cert.key"

3.3 啟動Filebeat

  由於Filebeat在安裝時已經註冊為systemd的服務,所以只需要直接啟動即可:

systemctl start filebeat.service

  檢查Filebeat啟動狀態:

systemctl status filebeat.service

3.4 驗證Filebeat

  通過訪問ElasticSearch API可以發現以下變化:ES建立了以filebeat-開頭的索引,我們還能夠看到其來源及具體的message。

四、Kibana配置

  接下來我們就要告訴Kibana,要查詢和分析ElasticSearch中的哪些日誌,因此需要配置一個Index Pattern。從Filebeat中我們知道Index是filebeat-timestamp這種格式,因此這裏我們定義Index Pattern為 filebeat-*

  點擊Next Step,這裏我們選擇Time Filter field name為@timestamp:

  單擊Create index pattern按鈕,即可完成配置。

  這時我們單擊Kibana左側的Discover菜單,即可看到容器的日誌信息啦:

  仔細看看細節,我們關注一下message字段:

  可以看到,我們重點要關注的是message,因此我們也可以篩選一下只看這個字段的信息:

  此外,Kibana還提供了搜索關鍵詞的日誌功能,例如這裏我關注一下日誌中包含unhandled exception(未處理異常)的日誌信息:

  這裏只是樸素的展示了導入ELK的日誌信息,實際上ELK還有很多很豐富的玩法,例如分析聚合、炫酷Dashboard等等。筆者在這裏也是初步使用,就介紹到這裏啦。

五、Fluentd引入

5.1 關於Fluentd

  前面我們採用的是Filebeat收集Docker的日誌信息,基於Docker默認的json-file這個logging driver,這裏我們改用Fluentd這個開源項目來替換json-file收集容器的日誌。

  Fluentd是一個開源的數據收集器,專為處理數據流設計,使用JSON作為數據格式。它採用了插件式的架構,具有高可擴展性高可用性,同時還實現了高可靠的信息轉發。Fluentd也是雲原生基金會 (CNCF) 的成員項目之一,遵循Apache 2 License協議,其github地址為:。Fluentd與Logstash相比,比佔用內存更少、社區更活躍,兩者的對比可以參考這篇文章《》。

  因此,整個日誌收集與處理流程變為下圖,我們用 Filebeat 將 Fluentd 收集到的日誌轉發給 Elasticsearch。

   當然,我們也可以使用Fluentd的插件(fluent-plugin-elasticsearch)直接將日誌發送給 Elasticsearch,可以根據自己的需要替換掉Filebeat,從而形成Fluentd => ElasticSearch => Kibana 的架構,也稱作EFK。

5.2 運行Fluentd

  這裏我們通過容器來運行一個Fluentd採集器:

docker run -d -p 24224:24224 -p 24224:24224/udp -v /edc/fluentd/log:/fluentd/log fluent/fluentd

  默認Fluentd會使用24224端口,其日誌會收集在我們映射的路徑下。

  此外,我們還需要修改Filebeat的配置文件,將/edc/fluentd/log加入監控目錄下:

#=========================== Filebeat inputs =============================

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /edc/fluentd/log/*.log

  添加監控配置之後,需要重新restart一下filebeat:

systemctl restart filebeat

5.3 運行測試容器

  為了驗證效果,這裏我們Run兩個容器,並分別制定其log-dirver為fluentd:

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-A" \
           busybox sh -c 'while true; do echo "This is a log message from container A"; sleep 10; done;'

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-B" \
           busybox sh -c 'while true; do echo "This is a log message from container B"; sleep 10; done;'

  這裏通過指定容器的log-driver,以及為每個容器設立了tag,方便我們後面驗證查看日誌。

5.4 驗證EFK效果

  這時再次進入Kibana中查看日誌信息,便可以通過剛剛設置的tag信息篩選到剛剛添加的容器的日誌信息了:

六、小結

  本文從ELK的基本組成入手,介紹了ELK的基本處理流程,以及從0開始搭建了一個ELK環境,演示了基於Filebeat收集容器日誌信息的案例。然後,通過引入Fluentd這個開源數據收集器,演示了如何基於EFK的日誌收集案例。當然,ELK/EFK有很多的知識點,筆者也還只是初步使用,希望未來能夠分享更多的實踐總結。

參考資料

CloudMan,《》

一杯甜酒,《》

於老三,《》

zpei0411,《》

曹林華,《》

 

作者:

出處:

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

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

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

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

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

如何打造一款m3u8視頻爬蟲

0.前言

m3u8是一種很常見的網頁視頻播放器的視頻源,比如說中國大學MOOC中課程就是使用了該種視頻格式。

隨便打開一門課程,就可以發現在網絡請求中存在一個m3u8的文件,在preview中預覽,它並不像我們想象中是亂碼的視頻流。

裏面是一個列表,有一堆ts結尾的文件名,每個下面還跟了一個EXTINF的字段,好像是時間,在我們播放視頻時,網絡請求中會不斷出現請求ts的內容。

隨便打開一個ts文件,它的內容卻是如圖視頻流一般亂碼的。

說到這裏,你可能有猜測了,m3u8並不是視頻流的文件,而有可能是組織ts文件的規範,EXTINF代表播放每多少秒去請求下一片ts流。

這種邊看邊加載的方法無疑可以減少我們的網絡負荷。

要用爬蟲爬取這類視頻的方法也很簡單,我們只需要獲得m3u8文件,就可以得到視頻的ts地址了,將所有ts請求下來之後進行合併,就可以得到視頻文件了。

不過要提的一點是,很多視頻網站會對他們的ts進行加密,我們下載下來合併之後可能視頻能看,但是播放器放着放着就卡住了,然後之後黑屏畫面。

1.編碼部分

我們先根據m3u8來判斷一下創建咋樣一個代表M3U8視頻對象的類。

我們首先需要定義一個list,來存放這個m3u8視頻下所有的ts文件,也就是後面說到的TS類。

這裏提一點,m3u8裏面的ts的路徑一般對路徑,會和m3u8在同一文件夾,我們代碼中也是這麼認為了,但是難免有些網站會單獨存放m3u8和ts文件,如果遇到這種情況,修改一下代碼即可。

有了ts的名稱,我們還需要URL的前綴,也就是圖中紫色劃線部分,也就是basepath。

此外,我們還需要一個TS對象。

這個對象中存儲TS文件名稱以及時間EXTINF。

定義完實體類,就需要編寫下載視頻的過程了。

首先需要請求到m3u8的文件,此處使用Java的HttpURLConnection來請求獲取,其它語言類似,只需要請求到文件即可。

請求到了m3u8的文本內容,我們還需要解析它 ,從中得到ts的名稱。

得到了M3U8視頻對象之後,我們就可以遍歷請求它的list中TS對象的名稱屬性來下載ts文件了。

這麼多ts文件如果我們在單線程中遍歷請求,會很耗費時間,Java給我們提供了Stream,其中parallel可以讓我們併發去遍歷集合,效率會提升不少。

依舊是使用HttpURLConnection來做請求,不過最好本次設置超時時間。

這樣就可以請求到所有ts文件了。

最後要做的就是合併這些ts文件成為一個MP4文件。

對於未加密的正常ts文件,我們只需要按照編號順序直接拼接即可。

這樣就算是完成了M3U8視頻抓取了。

2.打包使用

下載地址:

在命令行中java -jar m3u8-down.jar [m3u8地址],會显示報錯信息。

也可以直接m3u8-down.jar [m3u8地址],不會显示保存信息,會在後台執行。

最終會在同目錄下生成一個output.mp4的文件,temp文件可以刪除。

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

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

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

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

[ch02-01] 線性反向傳播

系列博客,原文在筆者所維護的github上:,
點擊star加星不要吝嗇,星越多筆者越努力。

2.1 線性反向傳播

2.1.1 正向計算的實例

假設我們有一個函數:

\[z = x \cdot y \tag{1}\]

其中:

\[x = 2w + 3b \tag{2}\]

\[y = 2b + 1 \tag{3}\]

計算圖如圖2-4。

圖2-4 簡單線性計算的計算圖

注意這裏x, y, z不是變量,只是計算結果。w, b是才變量。因為在後面要學習的神經網絡中,我們要最終求解的是w和b的值,在這裏先預熱一下。

當w = 3, b = 4時,會得到圖2-5的結果。

圖2-5 計算結果

最終的z值,受到了前面很多因素的影響:變量w,變量b,計算式x,計算式y。常數是個定值,不考慮。

2.1.2 反向傳播求解w

求w的偏導

目前的z=162,如果我們想讓z變小一些,比如目標是z=150,w應該如何變化呢?為了簡化問題,我們先只考慮改變w的值,而令b值固定為4。

如果想解決這個問題,我們可以在輸入端一點一點的試,把w變成4試試,再變成3.5試試……直到滿意為止。現在我們將要學習一個更好的解決辦法:反向傳播。

我們從z開始一層一層向回看,圖中各節點關於變量w的偏導計算結果如下:

\[因為z = x \cdot y,其中x = 2w + 3b,y = 2b + 1\]

所以:

\[\frac{\partial{z}}{\partial{w}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{w}}=y \cdot 2=18 \tag{4}\]

其中:

\[\frac{\partial{z}}{\partial{x}}=\frac{\partial{}}{\partial{x}}(x \cdot y)=y=9\]

\[\frac{\partial{x}}{\partial{w}}=\frac{\partial{}}{\partial{w}}(2w+3b)=2\]

圖2-6 對w的偏導求解過程

圖2-6其實就是鏈式法則的具體表現,z的誤差通過中間的x傳遞到w。如果不是用鏈式法則,而是直接用z的表達式計算對w的偏導數,會是什麼樣呢?我們來試驗一下。

根據公式1、2、3,我們有:

\[z=x \cdot y=(2w+3b)(2b+1)=4wb+2w+6b^2+3b \tag{5}\]

對上式求w的偏導:

\[ {\partial z \over \partial w}=4b+2=4 \cdot 4 + 2=18 \tag{6} \]

公式4和公式6的結果完全一致!所以,請大家相信鏈式法則的科學性。

求w的具體變化值

公式4和公式6的含義是:當w變化一點點時,z會發生w的變化值的18倍的變化。記住我們的目標是讓z=150,目前在初始狀態時是162,所以,問題轉化為:當我們需要z從162變到150時,w需要變化多少?

既然:

\[ \Delta z = 18 \cdot \Delta w \]

則:

\[ \Delta w = {\Delta z \over 18}={162-150 \over 18}= 0.6667 \]

所以:

\[w = w – 0.6667=2.3333\]
\[x=2w+3b=16.6667\]
\[z=x \cdot y=16.6667 \times 9=150.0003\]

我們一下子就成功地讓z值變成了150.0003,與150的目標非常地接近,這就是偏導數的威力所在。

【課堂練習】推導z對b的偏導數,結果在下一小節中使用

2.1.3 反向傳播求解b

求b的偏導

這次我們令w的值固定為3,變化b的值,目標還是讓z=150。同上一小節一樣,先求b的偏導數。

注意,在上一小節中,求w的導數只經過了一條路:從z到x到w。但是求b的導數時要經過兩條路,如圖2-7所示:

  1. 從z到x到b
  2. 從z到y到b

圖2-7 對b的偏導求解過程

從複合導數公式來看,這兩者應該是相加的關係,所以有:

\[\frac{\partial{z}}{\partial{b}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{b}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{b}}=y \cdot 3+x \cdot 2=63 \tag{7}\]

其中:

\[\frac{\partial{z}}{\partial{x}}=\frac{\partial{}}{\partial{x}}(x \cdot y)=y=9\]
\[\frac{\partial{z}}{\partial{y}}=\frac{\partial{}}{\partial{y}}(x \cdot y)=x=18\]
\[\frac{\partial{x}}{\partial{b}}=\frac{\partial{}}{\partial{b}}(2w+3b)=3\]
\[\frac{\partial{y}}{\partial{b}}=\frac{\partial{}}{\partial{b}}(2b+1)=2\]

我們不妨再驗證一下鏈式求導的正確性。把公式5再拿過來:

\[z=x \cdot y=(2w+3b)(2b+1)=4wb+2w+6b^2+3b \tag{5}\]

對上式求b的偏導:

\[ {\partial z \over \partial b}=4w+12b+3=12+48+3=63 \tag{8} \]

結果和公式7的鏈式法則一樣。

求b的具體變化值

公式7和公式8的含義是:當b變化一點點時,z會發生b的變化值的63倍的變化。記住我們的目標是讓z=150,目前在初始狀態時是162,所以,問題轉化為:當我們需要z從162變到150時,b需要變化多少?

既然:

\[\Delta z = 63 \cdot \Delta b\]

則:

\[ \Delta b = {\Delta z \over 63}={162-150 \over 63}=​0.1905 \]

所以:
\[ b=b-0.1905=3.8095 \]
\[x=2w+3b=17.4285\]
\[y=2b+1=8.619\]
\[z=x \cdot y=17.4285 \times 8.619=150.2162\]

這個結果也是與150很接近了,但是精度還不夠。再迭代幾次,應該可以近似等於150了,直到誤差不大於1e-4時,我們就可以結束迭代了,對於計算機來說,這些運算的執行速度很快。

【課題練習】請自己嘗試手動繼續迭代兩次,看看誤差的精度可以達到多少?

這個問題用數學公式倒推求解一個二次方程,就能直接得到準確的b值嗎?是的!但是我們是要說明機器學習的方法,機器並不會解二次方程,而且很多時候不是用二次方程就能解決實際問題的。而上例所示,是用機器所擅長的迭代計算的方法來不斷逼近真實解,這就是機器學習的真諦!而且這種方法是普遍適用的。

2.1.4 同時求解w和b的變化值

這次我們要同時改變w和b,到達最終結果為z=150的目的。

已知\(\Delta z=12\),我們不妨把這個誤差的一半算在w賬上,另外一半算在b的賬上:

\[\Delta b=\frac{\Delta z / 2}{63} = \frac{12/2}{63}=0.095\]

\[\Delta w=\frac{\Delta z / 2}{18} = \frac{12/2}{18}=0.333\]

  • \(w = w-\Delta w=3-0.333=2.667\)
  • \(b = b – \Delta b=4-0.095=3.905\)
  • \(x=2w+3b=2 \times 2.667+3 \times 3.905=17.049\)
  • \(y=2b+1=2 \times 3.905+1=8.81\)
  • \(z=x \times y=17.049 \times 8.81=150.2\)

【課堂練習】用Python代碼實現以上雙變量的反向傳播計算過程

容易出現的問題:

  1. 在檢查Δz時的值時,注意要用絕對值,因為有可能是個負數
  2. 在計算Δb和Δw時,第一次時,它們對z的貢獻值分別是1/63和1/18,但是第二次時,由於b和w值的變化,對於z的貢獻值也會有微小變化,所以要重新計算。具體解釋如下:

\[ \frac{\partial{z}}{\partial{b}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{b}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{b}}=y \cdot 3+x \cdot 2=3y+2x \]
\[ \frac{\partial{z}}{\partial{w}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{w}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{w}}=y \cdot 2+x \cdot 0 = 2y \]
所以,在每次迭代中,要重新計算下面兩個值:
\[ \Delta b=\frac{\Delta z}{3y+2x} \]
\[ \Delta w=\frac{\Delta z}{2y} \]

以下是程序的運行結果。

沒有在迭代中重新計算Δb的貢獻值:

single variable: b -----
w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
delta_b=0.190476
w=3.000000,b=3.809524,z=150.217687,delta_z=0.217687
delta_b=0.003455
w=3.000000,b=3.806068,z=150.007970,delta_z=0.007970
delta_b=0.000127
w=3.000000,b=3.805942,z=150.000294,delta_z=0.000294
delta_b=0.000005
w=3.000000,b=3.805937,z=150.000011,delta_z=0.000011
delta_b=0.000000
w=3.000000,b=3.805937,z=150.000000,delta_z=0.000000
done!
final b=3.805937

在每次迭代中都重新計算Δb的貢獻值:

single variable new: b -----
w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
factor_b=63.000000, delta_b=0.190476
w=3.000000,b=3.809524,z=150.217687,delta_z=0.217687
factor_b=60.714286, delta_b=0.003585
w=3.000000,b=3.805938,z=150.000077,delta_z=0.000077
factor_b=60.671261, delta_b=0.000001
w=3.000000,b=3.805937,z=150.000000,delta_z=0.000000
done!
final b=3.805937

從以上兩個結果對比中,可以看到三點:

  1. factor_b第一次是63,以後每次都會略微降低一些
  2. 第二個函數迭代了3次就結束了,而第一個函數迭代了5次,效率不一樣
  3. 最後得到的結果是一樣的,因為這個問題只有一個解

對於雙變量的迭代,有同樣的問題:

沒有在迭代中重新計算Δb,Δw的貢獻值(factor_b和factor_w每次都保持63和18):

double variable: w, b -----
w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
delta_b=0.095238, delta_w=0.333333
w=2.666667,b=3.904762,z=150.181406,delta_z=0.181406
delta_b=0.001440, delta_w=0.005039
w=2.661628,b=3.903322,z=150.005526,delta_z=0.005526
delta_b=0.000044, delta_w=0.000154
w=2.661474,b=3.903278,z=150.000170,delta_z=0.000170
delta_b=0.000001, delta_w=0.000005
w=2.661469,b=3.903277,z=150.000005,delta_z=0.000005
done!
final b=3.903277
final w=2.661469

在每次迭代中都重新計算Δb,Δw的貢獻值(factor_b和factor_w每次都變化):

double variable new: w, b -----
w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
factor_b=63.000000, factor_w=18.000000, delta_b=0.095238, delta_w=0.333333
w=2.666667,b=3.904762,z=150.181406,delta_z=0.181406
factor_b=60.523810, factor_w=17.619048, delta_b=0.001499, delta_w=0.005148
w=2.661519,b=3.903263,z=150.000044,delta_z=0.000044
factor_b=60.485234, factor_w=17.613053, delta_b=0.000000, delta_w=0.000001
w=2.661517,b=3.903263,z=150.000000,delta_z=0.000000
done!
final b=3.903263
final w=2.661517

這個與第一個單變量迭代不同的地方是:這個問題可以有多個解,所以兩種方式都可以得到各自的正確解,但是第二種方式效率高,而且滿足梯度下降的概念。

參考資料

代碼位置

ch02, Level1

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

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

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

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

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

.NET Core 3.0中用 Code-First 方式創建 gRPC 服務與客戶端

.NET Core love gRPC

千呼萬喚的 .NET Core 3.0 終於在 9 月份正式發布,在它的眾多新特性中,除了性能得到了大大提高,比較受關注的應該是 ASP.NET Core 3.0 對 gRPC 的集成了。
它的源碼託管在 grpc-dotnet 這個 Github 庫中,由微軟 .NET 團隊與谷歌 gRPC 團隊共同維護.

.NET Core 對 gRPC 的支持在 grpc 官方倉庫早已有實現(grpc/csharp),但服務端沒有很好地與 ASP.NET Core 集成,使用起來還需要自己進行一些集成擴展。
而 ASP.NET Core 3.0 新增了 gRPC 服務的託管功能,能讓 gRPC 與 ASP.NET Core 框架本身的特性很好地結合,如日誌、依賴注入、身份認證和授權,並由 Kestrel 服務器提供 HTTP/2 鏈接,性能上得到充分保障。

推薦把項目中已有的 RPC 框架或者內部服務間 REST 調用都遷移到 gRPC 上,因為它已經是雲原生應用的標準 RPC 框架,在整個 CNCF 主導下的雲原生應用開發生態里 gRpc 有着舉足輕重的地位。

對於 gRPC 的使用方式,前段時間已經有其他大神寫的幾篇文章了,這裏就不再贅述了。
本文主要介紹的是區別於標準使用規範的,但對.NET 應用更加友好的使用方式,最後會提供源碼來展示。

作為對比,還是要列一下標準的使用步驟:

  1. 定義 proto 文件,包含服務、方法、消息對象的定義
  2. 引入 Grpc.Tools Nuget 包並添加指定 proto 路徑和生成模式
  3. 生成項目,得到服務端的抽象類或客戶端的調用客戶端組件
  4. 實現服務端抽象類,並在 ASP.NET Core 註冊這個服務的路由端點
  5. DI 註冊 gRPC 服務。
  6. 客戶端用 Grpc.Net.ClientFactory Nuget 包進行統一配置和依賴注入

.NET Core 對 gRPC 的大力支持使開發者開發效率大大提高,入門難度也減少了許多,完全可以成為跟 WebApi 等一樣的 .NET Core 技術棧的標配。

proto 在單一語言系統架構中的局限性

使用 proto 文件的好處是多語言支持,同一份 proto 可以生成各種語言的服務和客戶端,可以讓用不同語言開發的微服務直接互相遠程調用。但 proto 文件作為不同服務間的契約,不可以經常修改,否則就會對使用了它的服務造成不同程度的影響,因此對 proto 文件的版本控制需要得到重視。

另外,我們的應用程序還不應該與 gRPC 耦合,否則就會導致系統架構被這些實現細節所綁架。直接依賴 proto 文件和由它生成的代碼,就是對 gRPC 的強耦合。

例如,當應用程序在演進的過程中,複雜度還未達到完全部署隔離的必要時,為了避免因“完全邊界”引入的部署運維複雜性,又能預留隔離的可能性,需要有一層接口層作為“不完全邊界”。

又比如,目前在 windows 系統的 iis 上還不支持 grpc-dotnet,當有 windows 上的應用程序需要使用 RPC,就需要換成 REST 的實現了。

因此,為了不讓應用程序對 gRPC 過於依賴,還應該使用一層抽象(接口)層與其解耦,用接口來隔離對 RPC 實現的依賴,這樣在需要使用不同的實現時,可以通過註冊不同的實現來方便地切換。

在這些場景下,本文要介紹的 Code-First gRPC 使用方法就發揮作用了。

Code-First gRPC

說了這麼久,我好像還沒正式介紹 Code-First gRPC,到底他有多適合在單一語言系統架構中實現 gRPC 呢?下面要介紹的就是基於大名鼎鼎的 protobuf-net 實現的 gRPC 框架,protobuf-net.Grpc

protobuf-net 是在過去十幾年前到現在一直在 .NET 中有名的 Protobuf 庫,想用 Protobuf 序列化時就會用到這個庫。他的特性就是可以把 C# 代碼編寫的類能以 Protobuf 的協議進行序列化和反序列化,而不是 proto 文件再生成這些類。而 protobuf-net.Grpc 則是一脈相承,可以把 C# 寫的接口,在服務端方便地把接口的實現類註冊成 ASP.NET Core 的 gRPC 服務,在客戶端把接口動態代理實現為調用客戶端,調用前面的這個服務端。

用法很簡單,只要聲明一個接口為您的服務契約:

[ServiceContract]
public interface IMyAmazingService {
    ValueTask<SearchResponse> SearchAsync(SearchRequest request);
    // ...
}

然後實現該接口的服務端:

public class MyServer : IMyAmazingService {
    // ...
}

或者向系統獲取客戶端:

var client = http.CreateGrpcService<IMyAmazingService>();
var results = await client.SearchAsync(request);

這相當於以下 .proto 中的服務:

service MyAmazingService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
    // ...
}

protobuf-net.Grpc 同樣通過普通類型定義支持 gRPC 的四種模式,把 C# 8.0 中最新的 IAsyncEnumerable 類型識別成 proto 中的 stream,單向流、雙向流都可以實現!而且用 IAsyncEnumerable 實現可比 proto 生成的類方便很多。

例如 proto 雙向流定義:

rpc chat(stream ChatRequest) returns ( stream ChatResponse);

生成出來的方法是:

Task BathTheCat(IAsyncStreamReader<ChatRequest> requestStream, IServerStreamWriter<ChatResponse> responseStream)

protobuf-net.Grpc 只要定義一個方法:

IAsyncEnumerable<ChatResponse> SubscribeAsync(IAsyncEnumerable<ChatRequest> requestStream);

由此可見,protobuf-net.Grpc 無需在契約層引入第三方庫,充分運用了 C# 類型系統,把方法、類型映射到兼容了 gRPC 的服務定義上。

上文所說的 proto 局限也迎刃而解了,函數調用、gRPC、REST 都能方便切換。(REST 實現可以參考我的開源框架 shriek-fx 中的 Shriek.ServiceProxy.Http )組件。

下一篇,我將主要介紹利用 protobuf-net.Grpc 的 gRPC 雙向流模式與 Blazor 實現一個簡單的在線即時聊天室。

相關鏈接:

  • protobuf-net.Grpc:
  • shriek-fx:
  • GrpcChat:

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

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

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

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

Lombok 使用詳解,簡化Java編程

前言

在 Java 應用程序中存在許多重複相似的、生成之後幾乎不對其做更改的代碼,但是我們還不得不花費很多精力編寫它們來滿足 Java 的編譯需求

比如,在 Java 應用程序開發中,我們幾乎要為所有 Bean 的成員變量添加 get() ,set() 等方法,這些相對固定但又不得不編寫的代碼浪費程序員很多精力,同時讓類內容看着更雜亂,我們希望將有限的精力關注在更重要的地方。

Lombok 已經誕生很久了,甚至在 Spring Boot Initalizr 中都已加入了 Lombok 選項,

這裏我們將 Lombok 做一下詳細說明:

Lombok

官網的介紹:Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again. Early access to future java features such as val, and much more.

直白的說: Lombok 是一種 Java™ 實用工具,可用來幫助開發人員消除 Java 的冗長,尤其是對於簡單的 Java 對象(POJO)。它通過註解實現這一目的,且看:

Bean 的對比

傳統的 POJO 類是這樣的

通過Lombok改造后的 POJO 類是這樣的

一眼可以觀察出來我們在編寫 Employee 這個類的時候通過 @Data 註解就已經實現了所有成員變量的 get()set() 方法等,同時 Employee 類看起來更加清晰簡潔。Lombok 的神奇之處不止這些,豐富的註解滿足了我們開發的多數需求。

Lombok的安裝

查看下圖,@Data的實現,我們發現這個註解是應用在編譯階段的

這和我們大多數使用的註解,如 Spring 的註解(在運行時,通過反射來實現業務邏輯)是有很大差別的,如Spring 的@RestController 註解

一個更直接的體現就是,普通的包在引用之後一般的 IDE 都能夠自動識別語法,但是 Lombok 的這些註解,一般的 IDE 都無法自動識別,因此如果要使用 Lombok 的話還需要配合安裝相應的插件來支持 IDE 的編譯,防止IDE 的自動檢查報錯,下面以 IntelliJ IDEA 舉例安裝插件。

在Repositories中搜索Lombok,安裝后重啟IDE即可

在Maven或Gradle工程中添加依賴

至此我們就可以應用 Lombok 提供的註解幹些事情了。

Lombok註解詳解

Lombok官網提供了許多註解,但是 “勁酒雖好,可不要貪杯哦”,接下來逐一講解官網推薦使用的註解(有些註解和原有Java編寫方式沒太大差別的也沒有在此處列舉,如@ Synchronized等)

@Getter和@Setter

該註解可應用在類或成員變量之上,和我們預想的一樣,@Getter@Setter 就是為成員變量自動生成 get 和 set 方法,默認生成訪問權限為 public 方法,當然我們也可以指定訪問權限 protected 等,如下圖:

成員變量name指定生成set方法,並且訪問權限為protected;boolean類型的成員變量 female 只生成get方法,並修改方法名稱為 isFemale()。當把該註解應用在類上,默認為所有非靜態成員變量生成 get 和 set 方法,也可以通過 AccessLevel.NONE 手動禁止生成get或set方法,如下圖:

@ToString

該註解需應用在類上,為我們生成 Object 的 toString 方法,而該註解裏面的幾個屬性能更加豐富我們想要的內容, exclude 屬性禁止在 toString 方法中使用某字段,而of屬性可以指定需要使用的字段,如下圖:

查看編譯后的Employee.class得到我們預期的結果,如下圖

@EqualsAndHashCode

該註解需應用在類上,使用該註解,lombok會為我們生成 equals(Object other) 和 hashcode() 方法,包括所有非靜態屬性和非transient的屬性,同樣該註解也可以通過 exclude 屬性排除某些字段,of 屬性指定某些字段,也可以通過 callSuper 屬性在重寫的方法中使用父類的字段,這樣我們可以更靈活的定義bean的比對,如下圖:

查看編譯后的Employee.class文件,如下圖:

@NonNull

該註解需應用在方法或構造器的參數上或屬性上,用來判斷參數的合法性,默認拋出 NullPointerException 異常

查看NonNullExample.class文件,會為我們拋出空指針異常,如下圖:

當然我們可以通過指定異常類型拋出其他異常,lombok.nonNull.exceptionType = [NullPointerException | IllegalArgumentException] , 為實現此功能我們需要在項目的根目錄新建lombok.config文件:

重新編譯NonNullExample類,已經為我們拋出非法參數異常:

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

以上三個註解分別為我們生成無參構造器,指定參數構造器和包含所有參數的構造器,默認情況下,@RequiredArgsConstructor, @AllArgsConstructor 生成的構造器會對所有標記 @NonNull 的屬性做非空校驗。

無參構造器很好理解,我們主要看看后兩種,先看 @RequiredArgsConstructor

從上圖中我們可以看出, @RequiredArgsConstructor 註解生成有參數構造器時只會包含有 final 和 @NonNull 標識的 field,同時我們可以指定 staticName 通過生成靜態方法來構造對象

查看Employee.class文件

當我們把 staticName 屬性去掉我們來看遍以後的文件:

相信你已經注意到細節

@AllArgsConstructor 就更簡單了,請大家自行查看吧

@Data

介紹了以上的註解,再來介紹 @Data 就非常容易懂了,@Data 註解應用在類上,是@ToString, @EqualsAndHashCode, @Getter / @Setter@RequiredArgsConstructor合力的體現,如下圖:

@Builder

函數式編程或者說流式的操作越來越流行,應用在大多數語言中,讓程序更具更簡介,可讀性更高,編寫更連貫,@Builder就帶來了這個功能,生成一系列的builder API,該註解也需要應用在類上,看下面的例子就會更加清晰明了。

編譯后的Employee.class文件如下:

媽媽再也不用擔心我 set 值那麼麻煩了,流式操作搞定:

@Log

該註解需要應用到類上,在編寫服務層,需要添加一些日誌,以便定位問題,我們通常會定義一個靜態常量Logger,然後應用到我們想日誌的地方,現在一個註解就可以實現:

查看class文件,和我們預想的一樣:

Log有很多變種,CommonLog,Log4j,Log4j2,Slf4j等,lombok依舊良好的通過變種註解做良好的支持:

我實際使用的是 @Slf4j 註解

val

熟悉 Javascript 的同學都知道,var 可以定義任何類型的變量,而在 java 的實現中我們需要指定具體變量的類型,而 val 讓我們擺脫指定,編譯之後就精準匹配上類型,默認是 final 類型,就像 java8 的函數式表達式,()->System.out.println(“hello lombok”); 就可以解析到Runnable函數式接口。

查看解析后的class文件:

@Cleanup

當我們對流進行操作,我們通常需要調用 close 方法來關閉或結束某資源,而 @Cleanup 註解可以幫助我們調用 close 方法,並且放到 try/finally 處理塊中,如下圖:

編譯后的class文件如下,我們發現被try/finally包圍處理,並調用了流的close方法

其實在 JDK1.7 之後就有了 try-with-resource,不用我們顯式的關閉流,這個請大家自行看吧

總結

Lombok的基本操作流程是這樣的:

  1. 定義編譯期的註解
  2. 利用JSR269 api(Pluggable Annotation Processing API )創建編譯期的註解處理器
  3. 利用tools.jar的javac api處理AST(抽象語法樹)
  4. 將功能註冊進jar包

Lombok 當然還有很多註解,我推薦使用以上就足夠了,這個工具是帶來便利的,而不能被其捆綁,“弱水三千隻取一瓢飲,代碼千萬需抓重點看”,Lombok 能讓我更加專註有效代碼排除意義微小的障眼代碼(get,set等),另外Lombok生成的代碼還能像使用工具類一樣方便(@Builder)。

更多內容請查看官網:https://www.projectlombok.org/

靈魂追問

  1. 為什麼只有一個整體 @EqualsAndHashCode 註解?而不是 @Equals@HashCode?這涉及到一個規範哦
  2. 如果把三種構造器方式同時應用又加上了 @Builder 註解,會發生什麼?
  3. 你的燈還亮着嗎?

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注……

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

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

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

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