標籤: USB CONNECTOR

  • java中的string對象深入了解

    java中的string對象深入了解

    這裏來對Java中的String對象做一個稍微深入的了解。

    Java對象實現的演進

    String對象是Java中使用最頻繁的對象之一,所以Java開發者們也在不斷地對String對象的實現進行優化,以便提升String對象的性能。

    Java6以及之前版本中String對象的屬性

    在Java6以及之前版本中,String對象是對char數組進行了封裝實現的對象,其主要有4個成員成員變量,分別是char數組、偏移量offset、字符數量count和哈希值hash。String對象是通過offset和count兩個屬性來定位char[]數組,獲取字符串。這樣做可以高效、快速地共享數組對象,同時節省內存空間,但是這種方式卻可能會導致內存泄漏的發生。

    Java7、8版本中String對象的屬性

    從Java7版本開始,Java對String類做了一些改變,具體是String類不再有offset和count兩個變量了。這樣做的好處是String對象佔用的內存稍微少了點,同時String.substring()方法也不再共享char[]了,從而解決了使用該方法可能導致的內存泄漏問題。

    Java9以及之後版本中String對象的屬性

    從Java9版本開始,Java將char[]數組改為了byte[]數組。我們都知道,char是兩個字節的,如果用來存一個字節的情況下就會造成內存空間的浪費。而為了節約這一個字節的空間,Java開發者就改成了一個使用一個字節的byte來存儲字符串。

    另外,在Java9中,String對象維護了一個新的屬性coder,這個屬性是編碼格式的標識,在計算字符串長度或者調用indexOf()方法的時候,會需要根據這個字段去判斷如何計算字符串長度。coder屬性默認有0和1兩個值,其中0代表Latin-1(單字節編碼),1則表示UTF-16編碼。

    String對象的創建方式與在內存中的存放

    在Java中,對於基本數據類型的變量和對對象的引用,保存在棧內存的局部變量表中;而通過new關鍵字和Constructor創建的對象,則是保存在堆內存中。而String對象的創建方式一般為兩種,一種是字面量(字符串常量)的方式,一種則是構造函數(String())的方式,兩種方式在內存中的存放有所不同。

    字面量(字符串常量)的創建方式

    使用字面量的方式創建字符串時,JVM會在字符串常量池中先檢查是否存在該字面量,如果存在,則返回該字面量在內存中的引用地址;如果不存在,則在字符串常量池中創建該字面量並返回引用。使用這種方式創建的好處是避免了相同值的字符串在內存中被重複創建,節約了內存,同時這種寫法也會比較簡單易讀一些。

    String str = "i like yanggb.";

    字符串常量池

    這裏要特別說明一下常量池。常量池是JVM為了減少字符串對象的重複創建,特別維護了一個特殊的內存,這段內存被稱為字符串常量池或者字符串字面量池。在JDK1.6以及之前的版本中,運行時常量池是在方法區中的。在JDK1.7以及之後版本的JVM,已經將運行時常量池從方法區中移了出來,在Java堆(Heap)中開闢了一塊區域用來存放運行時常量池。而從JDK1.8開始,JVM取消了Java方法區,取而代之的是位於直接內存的元空間(MetaSpace)。總結就是,目前的字符串常量池在堆中。

    我們所知道的幾個String對象的特點都來源於String常量池。

    1.在常量池中會共享所有的String對象,因此String對象是不可被修改的,因為一旦被修改,就會導致所有引用此String對象的變量都隨之改變(引用改變),所以String對象是被設計為不可修改的,後面會對這個不可變的特性做一個深入的了解。

    2.String對象拼接字符串的性能較差的說法也是來源於此,因為String對象不可變的特性,每次修改(這裡是拼接)都是返回一個新的字符串對象,而不是再原有的字符串對象上做修改,因此創建新的String對象會消耗較多的性能(開闢另外的內存空間)。

    3.因為常量池中創建的String對象是共享的,因此使用雙引號聲明的String對象(字面量)會直接存儲在常量池中,如果該字面量在之前已存在,則是會直接引用已存在的String對象,這一點在上面已經描述過了,這裏再次提及,是為了特別說明這一做法保證了在常量池中的每個String對象都是唯一的,也就達到了節約內存的目的。

    構造函數(String())的創建方式

    使用構造函數的方式創建字符串時,JVM同樣會在字符串常量池中先檢查是否存在該字面量,只是檢查后的情況會和使用字面量創建的方式有所不同。如果存在,則會在堆中另外創建一個String對象,然後在這個String對象的內部引用該字面量,最後返回該String對象在內存地址中的引用;如果不存在,則會先在字符串常量池中創建該字面量,然後再在堆中創建一個String對象,然後再在這個String對象的內部引用該字面量,最後返回該String對象的引用。

    String str = new String("i like yanggb.");

    這就意味着,只要使用這種方式,構造函數都會另行在堆內存中開闢空間,創建一個新的String對象。具體的理解是,在字符串常量池中不存在對應的字面量的情況下,new String()會創建兩個對象,一個放入常量池中(字面量),一個放入堆內存中(字符串對象)。

    String對象的比較

    比較兩個String對象是否相等,通常是有【==】和【equals()】兩個方法。

    在基本數據類型中,只可以使用【==】,也就是比較他們的值是否相同;而對於對象(包括String)來說,【==】比較的是地址是否相同,【equals()】才是比較他們內容是否相同;而equals()是Object都擁有的一個函數,本身就要求對內部值進行比較。

    String str = "i like yanggb.";
    String str1 = new String("i like yanggb.");
    
    System.out.println(str == str1); // false
    System.out.println(str.equals(str1)); // true

    因為使用字面量方式創建的String對象和使用構造函數方式創建的String對象的內存地址是不同的,但是其中的內容卻是相同的,也就導致了上面的結果。

    String對象中的intern()方法

    我們都知道,String對象中有很多實用的方法。為什麼其他的方法都不說,這裏要特別說明這個intern()方法呢,因為其中的這個intern()方法最為特殊。它的特殊性在於,這個方法在業務場景中幾乎用不上,它的存在就是在為難程序員的,也可以說是為了幫助程序員了解JVM的內存結構而存在的(?我信你個鬼,你個糟老頭子壞得很)。

    /**
    * When the intern method is invoked, if the pool already contains a
    * string equal to this {@code String} object as determined by
    * the {@link #equals(Object)} method, then the string from the pool is
    * returned. Otherwise, this {@code String} object is added to the
    * pool and a reference to this {@code String} object is returned.
    **/
    public native String intern();

    上面是源碼中的intern()方法的官方註釋說明,大概意思就是intern()方法用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然後返回引用。然後我們可以從方法簽名上看出intern()方法是一個native方法。

    下面通過幾個例子來詳細了解下intern()方法的用法。

    第一個例子

    String str1 = new String("1");
    System.out.println(str1 == str1.intern()); // false
    System.out.println(str1 == "1"); // false

    在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中對象的引用,因此兩個打印語句的結果都是false。

    第二個例子

    String str2 = new String("2") + new String("3");
    System.out.println(str2 == str2.intern()); // true
    System.out.println(str2 == "23"); // true

    在上面的例子中,str2保存的是堆中一個String對象的引用,這和JVM對【+】的優化有關。實際上,在給str2賦值的第一條語句中,創建了3個對象,分別是在字符串常量池中創建的2和3、還有在堆中創建的字符串對象23。因為字符串常量池中不存在字符串對象23,所以這裏要特別注意:intern()方法在將堆中存在的字符串對象加入常量池的時候採取了一種截然不同的處理方案——不是在常量池中建立字面量,而是直接將該String對象自身的引用複製到常量池中,即常量池中保存的是堆中已存在的字符串對象的引用。根據前面的說法,這時候調用intern()方法,就會在字符串常量池中複製出一個對堆中已存在的字符串常量的引用,然後返回對字符串常量池中這個對堆中已存在的字符串常量池的引用的引用(就是那麼繞,你來咬我呀)。這樣,在調用intern()方法結束之後,返回結果的就是對堆中該String對象的引用,這時候使用【==】去比較,返回的結果就是true了。同樣的,常量池中的字面量23也不是真正意義的字面量23了,它真正的身份是堆中的那個String對象23。這樣的話,使用【==】去比較字面量23和str2,結果也就是true了。

    第三個例子

    String str4 = "45";
    String str3 = new String("4") + new String("5");
    System.out.println(str3 == str3.intern()); // false
    System.out.println(str3 == "45"); // false

    這個例子乍然看起來好像比前面的例子還要複雜,實際上卻和上面的第一個例子是一樣的,最難理解的反而是第二個例子。

    所以這裏就不多說了,而至於為什麼還要舉這個例子,我相信聰明的你一下子就明白了(我有醫保,你來打我呀)。

    String對象的不可變性

    先來看String對象的一段源碼。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    
        /** use serialVersionUID from JDK 1.0.2 for interoperability */
        private static final long serialVersionUID = -6849794470754667710L;
    }

    從類簽名上來看,String類用了final修飾符,這就意味着這個類是不能被繼承的,這是決定String對象不可變特性的第一點。從類中的數組char[] value來看,這個類成員變量被private和final修飾符修飾,這就意味着其數值一旦被初始化之後就不能再被更改了,這是決定String對象不可變特性的第二點。

    Java開發者為什麼要將String對象設置為不可變的,主要可以從以下三個方面去考慮:

    1.安全性。假設String對象是可變的,那麼String對象將可能被惡意修改。

    2.唯一性。這個做法可以保證hash屬性值不會頻繁變更,也就確保了唯一性,使得類似HashMap的容器才能實現相應的key-value緩存功能。

    3.功能性。可以實現字符串常量池(究竟是先有設計,還是先有實現呢)。

    String對象的優化

    字符串是常用的Java類型之一,所以對字符串的操作是避免不了的。而在對字符串的操作過程中,如果使用不當的話,性能可能會有天差地別,所以有一些地方是要注意一下的。

    拼接字符串的性能優化

    字符串的拼接是對字符串的操作中最頻繁的一個使用。由於我們都知道了String對象的不可變性,所以我們在開發過程中要盡量減少使用【+】進行字符串拼接操作。這是因為使用【+】進行字符串拼接,會在得到最終想要的結果前產生很多無用的對象。

    String str = 'i';
    str = str + ' ';
    str = str + 'like';
    str = str + ' ';
    str = str + 'yanggb';
    str = str + '.';
    
    System.out.println(str); // i like yanggb.

    事實上,如果我們使用的是比較智能的IDE編寫代碼的話,編譯器是會提示將代碼優化成使用StringBuilder或者StringBuffer對象來優化字符串的拼接性能的,因為StringBuilder和StringBuffer都是可變對象,也就避免了過程中產生無用的對象了。而這兩種替代方案的區別是,在需要線程安全的情況下,選用StringBuffer對象,這個對象是支持線程安全的;而在不需要線程安全的情況下,選用StringBuilder對象,因為StringBuilder對象的性能在這種場景下,要比StringBuffer對象或String對象要好得多。

    使用intern()方法優化內存佔用

    前面吐槽了intern()方法在實際開發中沒什麼用,這裏又來說使用intern()方法來優化內存佔用了,這人真的是,嘿嘿,真香。關於方法的使用就不說了,上面有詳盡的用法說明,這裏來說說具體的應用場景好了。有一位Twitter的工程師在Qcon全球軟件開發大會上分享了一個他們對String對象優化的案例,他們利用了這個String.intern()方法將以前需要20G內存存儲優化到只需要幾百兆內存。具體就是,使用intern()方法將原本需要創建到堆內存中的String對象都放到常量池中,因為常量池的不重複特性(存在則返回引用),也就避免了大量的重複String對象造成的內存浪費問題。

    什麼,要我給intern()方法道歉?不可能。String.intern()方法雖好,但是也是需要結合場景來使用的,並不能夠亂用。因為實際上,常量池的實現是類似於一個HashTable的實現方式,而HashTable存儲的數據越大,遍歷的時間複雜度就會增加。這就意味着,如果數據過大的話,整個字符串常量池的負擔就會大大增加,有可能性能不會得到提升卻反而有所下降。

    字符串分割的性能優化

    字符串的分割是字符串操作的常用操作之一,對於字符串的分割,大部分人使用的都是split()方法,split()方法在大部分場景下接收的參數都是正則表達式,這種分割方式本身沒有什麼問題,但是由於正則表達式的性能是非常不穩定的,使用不恰當的話可能會引起回溯問題並導致CPU的佔用居高不下。在以下兩種情況下split()方法不會使用正則表達式:

    1.傳入的參數長度為1,且不包含“.$|()[{^?*+\”regex元字符的情況下,不會使用正則表達式。

    2.傳入的參數長度為2,第一個字符是反斜杠,並且第二個字符不是ASCII数字或ASCII字母的情況下,不會使用正則表達式。

    所以我們在字符串分割時,應該慎重使用split()方法,而首先考慮使用String.indexOf()方法來進行字符串分割,在String.indexOf()無法滿足分割要求的時候再使用Split()方法。而在使用split()方法分割字符串時,需要格外注意回溯問題。

    總結

    雖然說在不了解String對象的情況下也能使用String對象進行開發,但是了解String對象可以幫助我們寫出更好的代碼。

     

    “只希望在故事的最後,我還是我,你也還是你。”

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

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

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

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

  • Docker基礎與實戰,看這一篇就夠了

    Docker基礎與實戰,看這一篇就夠了

    docker 基礎

    什麼是Docker

    Docker 使用 Google 公司推出的 Go 語言 進行開發實現,基於 Linux 內核的 cgroupnamespace,以及 AUFS 類的 Union FS 等技術,對進程進行封裝隔離,屬於 操作系統層面的虛擬化技術。由於隔離的進程獨立於宿主和其它的隔離的進程,因此也稱其為容器。

    Docker 在容器的基礎上,進行了進一步的封裝,從文件系統、網絡互聯到進程隔離等等,極大的簡化了容器的創建和維護。使得 Docker 技術比虛擬機技術更為輕便、快捷。

    記住最重要的一點,Dokcer實際是宿主機的一個普通的進程,這也是Dokcer與傳統虛擬化技術的最大不同。

    為什麼要使用Docker

    使用Docker最重要的一點就是Docker能保證運行環境的一致性,不會出現開發、測試、生產由於環境配置不一致導致的各種問題,一次配置多次運行。使用Docker,可更快地打包、測試以及部署應用程序,並可減少從編寫到部署運行代碼的周期。

    docker 安裝

    • Docker 要求 CentOS 系統的內核版本高於 3.10 ,查看本頁面的前提條件來驗證你的CentOS 版本是否支持 Docker 。
      uname -r

    • 更新yum,升級到最新版本
      yum update

    • 卸載老版本的docker(若有)
      yum remove docker docker-common docker-selinux docker-engine
      執行該命令只會卸載Docker本身,而不會刪除Docker存儲的文件,例如鏡像、容器、卷以及網絡文件等。這些文件保存在/var/lib/docker 目錄中,需要手動刪除。

    • 查看yum倉庫,查看是否有docker
      ll /etc/yum.repos.d/

      如果用的廠商的服務器(阿里雲、騰訊雲)一般都會有docker倉庫,如果用的是虛擬機或者公司的服務器基本會沒有。

    • 安裝軟件包, yum-util 提供yum-config-manager功能,另外兩個是devicemapper驅動依賴的
      yum install -y yum-utils device-mapper-persistent-data lvm2

    • 安裝倉庫
      yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    • 查看docker版本
      yum list docker-ce --showduplicates | sort -r

    • 安裝docker
      yum install docker-ce
      以上語句是是安裝最新版本的Docker,你也可以通過yum install docker-ce-<VERSION> 安裝指定版本

    • 啟動docker
      systemctl start docker

    • 驗證安裝是否正確
      dokcer run hello-world

    docker 重要命令

    鏡像相關

    • 搜索鏡像docker search
      docker search nginx Docker就會在Docker Hub中搜索含有“nginx”這個關鍵詞的鏡像倉庫

    • 下載鏡像docker pull
      docker pull nginx Docker就會在Docker Hub中下載含有“nginx”最新版本的鏡像
      當然也可以使用docker pull reg.jianzh5.com/nginx:1.7.9 下載指定倉庫地址標籤的nginx鏡像

    • 列出鏡像docker images

    • 刪除鏡像docker rmi
      docker rmi hello-world刪除我們剛剛下載的hello-world鏡像

    • 構建鏡像docker build
      通過Dockerfile構建鏡像,這個我們等下再拿出來詳細說明。

    容器相關

    • 新建啟動鏡像docker run
      這個命令是我們最常用的命令,主要使用以下幾個選項
      ① -d選項:表示後台運行
      ② -P選項(大寫):隨機端口映射
      ③ -p選項(小寫):指定端口映射,前面是宿主機端口後面是容器端口,如docker run nginx -p 8080:80,將容器的80端口映射到宿主機的8080端口,然後使用localhost:8080就可以查看容器中nginx的歡迎頁了
      ④ -v選項:掛載宿主機目錄,前面是宿主機目錄,後面是容器目錄,如docker run -d -p 80:80 -v /dockerData/nginx/conf/nginx.conf:/etc/nginx/nginx.conf nginx 掛載宿主機的/dockerData/nginx/conf/nginx.conf的文件,這樣就可以在宿主機對nginx進行參數配置了,注意目錄需要用絕對路徑,不要使用相對路徑,如果宿主機目錄不存在則會自動創建。
      ⑤–rm : 停止容器後會直接刪除容器,這個參數在測試是很有用,如docker run -d -p 80:80 --rm nginx
      ⑥–name : 給容器起個名字,否則會出現一長串的自定義名稱如 docker run -name niginx -d -p 80:80 - nginx

    • 列出容器 docker ps
      這個命令可以列出當前運行的容器,使用-a參數后列出所有的容器(包括已停止的)

    • 停止容器docker stop
      docker stop 5d034c6ea010 後面跟的是容器ID,也可以使用容器名稱

    • 啟動停止的容器docker start
      docker run是新建容器並啟動,docker start 是啟動停止的容器,如docker start 5d034c6ea010

    • 重啟容器docker restart
      此命令執行的過程實際是先執行docker stop,然後再執行docker start,如docker restart 5d034c6ea010

    • 進入容器docker exec -it 容器id /bin/bash
      docker exec -it 5d034c6ea010 /bin/bash,就相當於進入了容器本身的操作系統

    • 刪除容器 docker rm
      docker rm 5d034c6ea010 後面跟的是容器ID,刪除容器之前需要先停止容器運行

    • 數據拷貝docker cp
      此命令用於容器與宿主機之間進行數據拷貝,如 docker cp 5d034c6ea010: /etc/nginx/nginx.conf /dockerData/nginx/conf/nginx.conf 將容器的目錄文件拷貝到宿主機指定位置,容器ID可以替換成容器名。

    命令實戰

    如果我們需要一個nginx容器,並且需要在宿主機上直接修改nginx的配置文件、默認主頁,在宿主機可以實時看到容器nginx的日誌。我們可以按照如下的方式一步一步完成。

    • 使用–rm參數啟動容器,方便刪除
      docker run -d -p 8081:80 --name nginx --rm nginx

    • 進入容器,查看容器中配置文件、項目文件、日誌文件的目錄地址
      docker exec -it 9123b67e428e /bin/bash

    • 導出容器的配置文件
      docker cp nginx:/etc/nginx/nginx.conf /dockerData/nginx/conf/nginx.conf導出配置文件 nginx.conf
      docker cp nginx:/etc/nginx/conf.d /dockerData/nginx/conf/conf.d導出配置目錄 conf.d

    • 停止容器docker stop 9123b67e428e,由於加了–rm參數,容器會自動刪除

    • 再以如下命令啟動容器,完成目錄掛載
      shell docker run -d -p 8081:80 --name nginx \ -v /dockerData/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \ -v /dockerData/nginx/conf/conf.d:/etc/nginx/conf.d \ -v /dockerData/nginx/www:/usr/share/nginx/html \ -v /dockerData/nginx/logs:/var/log/nginx nginx
    • 訪問服務器地址http://192.168.136.129:8081/

      訪問報錯,這時候就進入宿主機的日誌目錄/dockerData/nginx/logs查看日誌
      2019/11/23 10:08:11 [error] 6#6: *1 directory index of “/usr/share/nginx/html/” is forbidden, client: 192.168.136.1, server: localhost, request: “GET / HTTP/1.1”, host: “192.168.136.129:8081”
      因為/usr/share/nginx/html/被掛載到了服務器上面的/dockerData/nginx/www目錄下,原來的歡迎頁面在dockerData/nginx/www是沒有的,所有就報錯了,這裏我們隨便建一個。

    • 建立默認主頁
      shell #打開項目文件 cd /dockerData/nginx/www #使用vim 創建並編輯文件 vi index.html #此時我們會進入vim界面,按 i 插入,然後輸入 <h1 align="center">Hello,Welcome to Docker World</h1> #輸入完后,按 esc,然後輸入 :wq
    • 再次訪問瀏覽器地址

    Dockerfile

    我們可以使用Dockfile構建一個鏡像,然後直接在docker中運行。Dockerfile文件為一個文本文件,裡面包含構建鏡像所需的所有的命令,首先我們來認識一下Dockerfile文件中幾個重要的指令。

    指令詳解

    • FROM
      選擇一個基礎鏡像,然後在基礎鏡像上進行修改,比如構建一個SpringBoot項目的鏡像,就需要選擇java這個基礎鏡像,FROM需要作為Dockerfile中的第一條指令
      如:FROM openjdk:8-jdk-alpine 基礎鏡像如果可以的話最好使用alpine版本的,採用alpline版本的基礎鏡像構建出來的鏡像會小很多。

    • RUN
      RUN指令用來執行命令行命令的。它有一下兩種格式:

      • shell 格式:RUN ,就像直接在命令行中輸入的命令一樣。 RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
      • exec 格式:RUN [“可執行文件”, “參數1”, “參數2”],這更像是函數調用中的格式。
    • CMD
      此指令就是用於指定默認的容器主進程的啟動命令的。
      CMD指令格式和RUN相似,也是兩種格式
      • shell 格式:CMD
      • exec 格式:CMD [“可執行文件”, “參數1”, “參數2″…]
      • 參數列表格式:CMD [“參數1”, “參數2″…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具體的參數。
    • ENTRYPOINT
      ENTRYPOINT 的格式和 RUN 指令格式一樣,分為 exec 格式和 shell 格式。 ENTRYPOINT 的目的和 CMD 一樣,都是在指定容器啟動程序及參數。ENTRYPOINT 在運行時也可以替代,不過比 CMD 要略顯繁瑣,需要通過 docker run 的參數 --entrypoint 來指定。
      當指定了 ENTRYPOINT 后,CMD 的含義就發生了改變,不再是直接的運行其命令,而是將 CMD 的內容作為參數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變為:
      <ENTRYPOINT> "<CMD>"

    • COPY & ADD
      這2個指令都是複製文件,它將從構建上下文目錄中   的文件/目錄 複製到新的一層的鏡像內的   位置。比如: COPY demo-test.jar app.jarADD demo-test.jar app.jar
      ADD指令比 COPY高級點,可以指定一個URL地址,這樣Docker引擎會去下載這個URL的文件,如果 ADD後面是一個 tar文件的話,Dokcer引擎還會去解壓縮。
      我們在構建鏡像時盡可能使用 COPY,因為 COPY 的語義很明確,就是複製文件而已,而 ADD 則包含了更複雜的功能,其行為也不一定很清晰。

    • EXPOSE
      聲明容器運行時的端口,這隻是一個聲明,在運行時並不會因為這個聲明應用就會開啟這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口。
      要將 EXPOSE 和在運行時使用 -p <宿主端口>:<容器端口> 區分開來。-p,是映射宿主端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問,而 EXPOSE 僅僅是聲明容器打算使用什麼端口而已,並不會自動在宿主進行端口映射。

    • ENV
      這個指令很簡單,就是設置環境變量,無論是後面的其它指令,如 RUN,還是運行時的應用,都可以直接使用這裏定義的環境變量。它有如下兩種格式:
      • ENV <key> <value>
      • ENV <key1>=<value1> <key2>=<value2>...
    • VOLUME
      該指令使容器中的一個目錄具有持久化存儲的功能,該目錄可被容器本身使用,也可共享給其他容器。當容器中的應用有持久化數據的需求時可以在Dockerfile中使用該指令。如VOLUME /tmp
      這裏的 /tmp 目錄就會在運行時自動掛載為匿名卷,任何向 /tmp 中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。當然,運行時可以覆蓋這個掛載設置。比如:
      docker run -d -v mydata:/tmp xxxx

    • LABEL
      你可以為你的鏡像添加labels,用來組織鏡像,記錄版本描述,或者其他原因,對應每個label,增加以LABEL開頭的行,和一個或者多個鍵值對。如下所示:
      LABEL version="1.0" LABEL description="test"

    Dockerfile實戰

    我們以一個簡單的SpringBoot項目為例構建基於SpringBoot應用的鏡像。
    功能很簡單,只是對外提供了一個say接口,在進入這個方法的時候打印出一行日誌,並將日誌寫入日誌文件。

    @SpringBootApplication
    @RestController
    @Log4j2
    public class DockerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DockerApplication.class, args);
        }
    
        @GetMapping("/say")
        public String say(){
            log.info("get say request...");
            return "Hello,Java日知錄";
        }
        
    }

    我們使用maven將其打包成jar文件,放入一個單獨的文件夾,然後按照下面步驟一步步構建鏡像並執行

    • 在當前文件夾建立Dockerfile文件,文件內容如下:
      properties FROM openjdk:8-jdk-alpine #將容器中的/tmp目錄作為持久化目錄 VOLUME /tmp #暴露端口 EXPOSE 8080 #複製文件 COPY docker-demo.jar app.jar #配置容器啟動后執行的命令 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
    • 使用如下命令構建鏡像
      docker built -t springboot:v1.0 .

      -t 指定鏡像的名稱及版本號,注意後面需要以 . 結尾。

    • 查看鏡像文件

    • 運行構建的鏡像
      docker run -v /app/docker/logs:/logs -p 8080:8080 --rm --name springboot springboot:v1.0

    • 瀏覽器訪問http://192.168.136.129:8080/say

    • 在宿主機上實時查看日誌
      tail -100f /app/docker/logs/docker-demo-info.log

      請關注個人公眾號:JAVA日知錄

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

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

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

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

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

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

  • 軟件架構模式

    軟件架構模式

    閱讀也花了較長的時間,大致也了解到整潔的架構要做到以下兩點:

    • well-isolated components:component是獨立部署的最小單元,由一系列遵循SOLID原則的module按照REP、CCP、CEP原則組成。
    • dependency rule:低層的detail去依賴高層的police

    但感覺並沒有對架構設計給出可行的參考。

    clean architecture 中的架構實例

    在的第34章 “The Missing Chapter”(由 Simon Brown 編寫)給出了一個具體的案例,用四種架構設計來實現一個 “online book store”。

    package by layer

    這是最常見的方案,從前往後分為:前端、後台(business logic)、持久化DB。

    優點是:簡單、容易上手,符合大多數公司的組織架構。

    存在的問題:

    • 軟件規模和複雜度增加時,三層架構就不夠了,需要重新考慮拆分;
    • 分層架構體現不出business domain;

    PACKAGE BY FEATURE

    垂直切分方案,所有的java代碼都放在一個package裏面

    好處在於凸顯domain concept

    PORTS AND ADAPTERS

    clean architecture這本書推薦的方案, 外層依賴於內層的domain

    PACKAGE BY COMPONENT

    本章作者 Simon Brown 提出的方案,service-centric view,將所有相關的職責打包稱一個粗粒度的Jar包

    bundling all of the responsibilities related to a single coarse-grained component into a single Java package

    看起來類似現在微服務的部署方式

    對於以上四種結構,依賴關係看起來是這樣的

    值得注意的是

    • 虛線箭頭表示component之間的依賴關係
    • PORTS AND ADAPTERS這種架構更能體現domain(business logic),即接口命名為Orders而不是OrdersRepository

    本章的作者最後還指出:++不管架構怎麼設計,粗心的implementation都可能違背最初的設計;依賴編譯器來保證架構的一以貫之,而不是自我約束或者事後檢查。++

    五種常見架構模式

    看完了clean architecture后,在網上搜索架構設計相關的書籍,發現了這本小冊子,篇幅很短,稱不上book,而是一個report。

    指出缺乏架構設計的軟件往往高度耦合,難以改變。因此,這本小冊子的目標就是介紹常用架構模式的特點、優點、缺點,幫助我們針對特定的業務需求做出合適的選擇。

    Layered Architecture

    分層架構也稱為n-tire architecture,這是最為常見的一種架構模式,一般從前往後分為四層:presentation, business, persistence, and database。如下圖所示:

    分層架構一般是一個新系統的最佳首選,因為其完美匹配傳統IT公司組織架構:一般的公司招人都是前端、後端、數據庫。

    分層架構的優點在於關注點隔離(separation of concerns),每一層做好自己這一層的職責,並向上一層提供服務即可,最為經典的案例就是七層網絡模型,這有利於開發、測試、管理與維護。

    分層架構中,需要注意的是兩個概念:closed layeropen layer

    closed layer的核心就是不要越層訪問,比如在上圖中,Presentation Layer就不應該跨國Business Layer直接去Persistence Layer訪問數據。

    A closed layer means that as a request moves from layer to layer, it must go through the layer right below it to get to the next layer below that one

    closed layer保證了層隔離( layers of isolation),使得某一層的修改影響的範圍盡可能小,比較可控。但closed layer有時候也會帶來一個問題:architecture sinkhole anti pattern(污水池反模式),具體是指,為了查簡單數據,層層轉發請求。比如為了在展示層显示玩家的某個數據,需要通過業務層、再到持久化層、再到DB層;取到數據再一層層傳遞迴來,在這個過程中,業務層並沒有對數據有邏輯上的處理。

    显示,污水池反模式衝擊了closed layer的美好想法。如何衡量這種層層轉發的請求是不是問題,可以參考80-20法則。

    如果80%是附帶邏輯的,那麼就是ok的,但如果有80% 是 simple passthrough processing,那麼就得考慮讓某些layer open。比如在複雜的業務系統中, 經常會有一些可復用的邏輯,這個時候會抽取為通用的服務層(service layer)。如下圖所示

    open layer 、close layer的概念可以幫助理清楚架構和請求流程之間的關係,讓架構師、程序員都清楚架構的邊界(boundary)在哪裡,重要的是,這個open-closed關係需要明確的文檔化,不要隨意打破,否則就會一團糟。

    Event-Driven Architecture

    The event-driven architecture pattern is a popular distributed asynchronous architecture pattern used to produce highly scalable applications.

    從上述定義可以看出事件驅動架構的幾個特點:分佈式、異步、可伸縮。其核心是:高度解耦合、專一職責的事件處理單元(Event Processor)

    事件驅動架構有兩種常見拓撲結構: the mediator and the broker.

    Mediator Topology

    需要一个中心化(全局唯一)的協調單元,用於組織一個事件中的多個步驟,這些步驟中有些是可并行的,有些必須是順序執行的,這就依賴Event Mediator的調度。如下圖所示

    Broker Topology
    這種是沒有中心的架構

    the message flow is distributed across the event processor components in a chain-like fashion through a lightweight message broker

    如下圖所示

    事件驅動的好處在於,高度可伸縮、便於部署、整體性能較好(得益於某些事件的併發執行)。但由於其分佈式異步的本性,其缺點也很明顯:開發比較複雜、維護成本較高;而且很難支持事務,尤其是一個邏輯事件跨越多個processor的時候。

    Microkernel Architecture

    微內核架構又稱之為插件式架構(plug-in architecture)。如下圖所示:

    微內核架構包含兩部分組件

    • a core system
    • plug-in modules.

    plug-in modules 是相互獨立的組件,用於增加、擴展 core system 的功能。

    這種架構非常適用於 product-based applications 即需要打包、下載、安裝的應用,比如桌面應用。最經典的例子就是Eclipse編輯器,玩遊戲的同學經常下載使用的MOD也可以看出插件。

    微內核架構通常可以是其他架構的一部分,以實現特定部分的漸進式設計、增量開發

    Microservices Architecture Pattern

    微服務架構並不是為了解決新問題而發明的新架構,而是從分層架構的單體式應用和SOA(service-oriented architecture)演化而來。

    微服務解決了分層架構潛在的成為單體式應用(Monolithic application)的問題:

    through the development of continuous delivery, separating the application into multiple deployable units

    同時,微服務還通過簡化(泛化)服務的概念,消除編排需求,簡化對服務組件的連接訪問。從而避免了SOA的各種缺點:複雜、昂貴、重度、難以理解和開發。

    The microservices architecture style addresses this complexity by simplifying the notion of a service, eliminating orchestration needs, and simplifying connectivity and access to service components.

    微服務架構如下:

    其核心是service component,這些服務組件相互解耦,易於獨立開發、部署。服務組件的粒度是微服務架構中最難的挑戰

    • 太大:失去了微服務架構的優勢
    • 太小:導致需要編排,或者服務組件間的通信、事務。

    而微服務架構相比SOA而言,其優勢就在於避免依賴和編排 — 編排引入大量的複雜工作。

    對於單個請求 如果service之間還要通信,那麼可能是就是粒度過小。解決辦法:

    • 如果通信是為了訪問數據:那麼可以通過共享db解決
    • 如果通信是為了使用功能:那麼可以考慮代碼的冗餘,雖然這違背了DRY原則。在clean architecture中也指出,component的自完備性有時候要高於代碼復用。

    Space-Based Architecture

    基於空間的架構,其核心目標是解決由於數據庫瓶頸導致的低伸縮性、低併發問題。

    分層架構中,在用戶規模激增的情況下,數據層的擴展往往會成為最後的瓶頸(相對而言,前端和業務邏輯都容易做成無狀態,比較好水平擴展)。而基於空間的架構的核心是內存複製,根本上解決了這個問題。

    High scalability is achieved by removing the central database constraint and using replicated in-memory data grids instead

    架構如下:

    其核心組件包括

    • processing unit,處理單元,其內部又包含一下組成
      • business logic
      • in-memory data grid
      • an optional asynchronous persistent store for failover
      • replication engine,用於同步數據修改
    • virtualized middleware
      • Messaging Grid: 監控processing unit可用性,路由客戶端請求到processing unit
      • Data Grid: 核心,負責processingunit之間的數據同步,毫秒級同步?
      • Processing Grid: 可選組件,如果一個請求需要多個processing unit的服務,那麼負責協調分發
      • Deployment Manager: 負責processing unit的按需啟停

    基於空間的架構很少見,而且從上面的核心組件描述來看的話,開發和維護應該都是比較負責的,由於是數據的同步這塊。而且由於數據都保存在內存中,那麼數據量就不能太大。

    基於空間的架構適用於需求變化大的小型web應用,不適用於有大量數據操作的傳統大規模關係型數據庫應用

    references

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

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

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

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

  • this綁定方式總結

    最近在回顧js的一些基礎知識,把《你不知道的js》系列又看了一遍,this始終是重中之重,還是決定把this相關知識做一個系統的總結,也方便自己日後回顧。

    this的四條綁定規則

    1.默認綁定

    這是最常用的函數調用類型:獨立函數調用(即函數是直接使用不帶任何修飾的函數引用進行調用的)。可以把這條規則看作是無法應用其他規則時的默認規則。
    默認綁定的this在非嚴格模式下指向window,嚴格模式下指向undefined,比如下面的函數foo在非嚴格模式下:

    var a = 2;
    function foo(){
        var a = 3;
        console.log(this.a);
    }
    foo(); //2

    這裏的foo()方法內的this指向了window,因此window.a = 2;

    嚴格模式下,this.指向undefined,因此訪問this.a會報錯:

    var a = 2;
    function foo(){
        "use strict";
        var a = 3;
        console.log(this.a);
    }
    foo(); //Uncaught TypeError: Cannot read property 'a' of undefined

    2.隱式綁定

    如果調用位置上有上下文對象,或者說被某個對象“擁有”或者“包
    含”,則使用隱式綁定。

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a: 2,
        foo: foo
    };
    obj.foo(); // 2

    上例中的foo是通過obj.foo()的方式調用的,調用位置會使用obj上下文來引用函數,因此foo中的this指向了obj。
    另外foo是當做引用被加入到obj中的,但是無論是直接在obj 中定義還是先定義再添加為引用屬性,foo嚴格上來說都不屬於obj,因此上述定義裏面的“擁有”與“包含”加上了引號,這樣說是為了方便理解。
    常見的隱式調用場景:
    obj.fn();
    arguments[i]();//其實就是將點的調用方式變為了[]調用
    el.onClick(function(){console.log(this);//this指向el})

    隱式丟失

    先來看一段代碼:

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo; // 函數別名!
    var a = "global"; // a 是全局對象的屬性
    bar(); // "global"

    上述代碼其實只用看調用的方式:bar(),這其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。
    還有一種參數傳遞的方式也會發生隱式丟失,原理其實跟上述例子一樣:

    function foo() {
        console.log( this.a );
    }
    function doFoo(fn) {
        // fn 其實引用的是foo
        fn(); // <-- 調用位置!
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = "global"; // a 是全局對象的屬性
    doFoo( obj.foo ); // "global"

    显示綁定

    使用call,apply和bind方法可以指定綁定函數的this的值,這種綁定方法叫显示綁定。

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a:2
    };
    foo.call( obj ); // 2

    通過foo.call(obj),我們可以在調用foo 時強制把它的this 綁定到obj 上

    new綁定

    new操作符可以基於一個“構造函數”新創建一個對象實例,new的實例化過程如下:

    1. 創建(或者說構造)一個全新的對象。
    2. 這個新對象會被執行[[ 原型]] 連接。
    3. 這個新對象會綁定到函數調用的this。
    4. 如果函數沒有返回其他對象,那麼new 表達式中的函數調用會自動返回這個新對象。
      明確了new的實例化過程后,思考如下代碼:
    function foo(a) {
        this.a = a;
    }
    var bar = new foo(2);
    console.log( bar.a ); // 2

    new foo(2)后新創建了個實例對象bar,然後把這個新對象bar綁定到了foo函數中的this,因此執行this.a = a后其實是把a賦給了bar.a

    優先級

    一般情況下this的綁定會根據上述四條綁定規則來,那麼他們同時出現時,該以怎樣的順序來判斷this的指向?下面是具體的規則:

    1. 函數是否在new 中調用(new 綁定)?如果是的話this 綁定的是新創建的對象( var bar = new foo() )。
    2. 函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是指定的對象( var bar = foo.call(obj2) )。
    3. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上下文對象。( var bar = obj1.foo() )
    4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。( var bar = foo() )

    綁定例外

    1.使用call,appy,bind這種顯式綁定的方法,參數傳入null或者undefined作為上下文時,函數調用還是會使用默認綁定

    function foo() {
        console.log( this.a );
    }
    var a = 2;
    foo.call( null ); // 2

    什麼情況下需要將上下文傳為null呢?
    1.使用bind函數來實現柯里化

    function foo(a,b) {
        console.log(a,b);
    }
    // 使用 bind(..) 進行柯里化
    var bar = foo.bind( null, 2 );
    bar( 3 ); // 2,3

    2.使用apply(..) 來展開一個數組,併當作參數傳入一個函數

    function foo(a,b) {
        console.log(a,b);
    }
    // 把數組展開成參數
    foo.apply( null, [2, 3] ); // 2,3

    其實上面兩種使用場景其實都不關心call/app/bind第一個參數的值是什麼,只是想傳個佔位值而已。
    但是總是傳入null可能會出現一些難以追蹤的bug,比如說當你在使用的第三方庫中的某個函數中有this時,this會被錯誤的綁定到全局對象上,造成一些難以預料的後果(修改全局變量)

    var a = 1;//全局變量
    const Utils = {
        a: 2,
        changeA: function(a){
            this.a = a;
        }
    }
    Utils.changeA(3);
    Utils.a //3
    a //1
    Utils.changeA.call(null,4);
    Utils.a //3
    a //4,修改了全局變量a!

    更安全的做法:

    var o = Object.create(null);
    Utils.changeA.call(o,6);
    a //1, 全局變量沒有修改
    o.a // 6 改的是變量o

    2.間接引用

    function foo() {
        console.log( this.a );
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    (p.foo = o.foo)(); // 2

    賦值表達式p.foo = o.foo 的返回值是目標函數的引用,因此調用位置是foo() 而不是p.foo() 或者o.foo()。根據我們之前說過的,這裡會應用默認綁定。

    this詞法(箭頭函數)

    上述的幾種規則適用於所有的正常函數,但不包括ES6的箭頭函數。箭頭函數不使用this的四種標準規則,而是根據外層(函數或者全局)作用域(詞法作用域)來決定this

    function foo() {
    // 返回一個箭頭函數
        return (a) => {
            //this 繼承自foo()
            console.log( this.a );
        };
    }
    var obj1 = {
        a:2
    };
    var obj2 = {
        a:3
    };
    var bar = foo.call( obj1 );
    bar.call( obj2 ); // 2, 不是3 !

    foo() 內部創建的箭頭函數會捕獲調用時foo() 的this。由於foo() 的this 綁定到obj1,bar(引用箭頭函數)的this 也會綁定到obj1,箭頭函數的綁定無法被修改。(new 也不行!)

    幾個例子加深理解

    this的理論知識講解得差不多了,來幾個例子看看自己有沒有理解全面:
    1.經典面試題:以下輸出結果是什麼

    var length = 10;
    function fn() {
        console.log(this.length);
    }
    var obj = {
      length: 5,
      method: function(fn) {
        fn();
        arguments[0]();
      }
    };
    obj.method(fn, 1);

    obj中method方法裏面調用了兩次fn。第一次是直接調用的“裸露”的fn,因此fn()中this使用默認綁定,this.length為10.第二次調用時通過arguments的方式調用的,arguments[0]其實指向的就是fn,但是是通過obj[fn]這種對象上下文的隱式綁定的,因此this指向arguments,而arguments只有一個一項(method中只有fn一個參數),因此arguments.length為1。因此打印的結果為:

    10
    1

    2.以下輸出什麼

    var obj = {
        birth: 1990,
        getAge: function () {
            var b = this.birth; // 1990
            var fn = function () {
                return new Date().getFullYear() - this.birth; // this指向window或undefined
            };
            return fn();
        }
    };
    obj.getAge();

    答案是嚴格模式下會報錯,非嚴格模式下輸出NaN
    原因也是因為在調用obj.getAge()后,getAge方法內的this使用隱式綁定。但是return fn()的時候用的是“裸露的fn”使用默認綁定,fn裏面的this指向window或者undefined。
    使用箭頭函數來修正this的指向:

    var obj = {
        birth: 1990,
        getAge: function () {
            var b = this.birth; // 1990
            var fn = () => new Date().getFullYear() - this.birth; // this指向obj對象
            return fn();
        }
    };
    obj.getAge(); // 25

    使用箭頭函數后,fn中的this在他的詞法分析階段就已經確定好了(即fn定義的時候),跟調用位置無關。fn的this指向外層的作用域(即getAge中的this)
    3.以下輸出為什麼是’luo’

    var A = function( name ){ 
        this.name = name;
    };
    var B = function(){ 
        A.apply(this,arguments);
    };
    B.prototype.getName = function(){ 
        return this.name;
    };
    var b=new B('sven');  // B {name: "luo"}
    console.log( b.getName() ); // 輸出:  'luo'

    執行new B(‘seven’)後會返回一個新對象b,並且B函數中的this會綁定到新對象b上,B的函數體內執行A.apply(this.arguments)也就是執行b.name = name;這個時候b的值就是{name:’luo’},所以b.getName()就能輸出’luo’啦~

    實際在業務使用中,邏輯會更複雜一些,但是萬變不離其宗,都按照上面寫的規則來代入就好了

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

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

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

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

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

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

    【其他文章推薦】

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

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

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

  • PL真有意思(三):名字、作用域和約束

    前言

    這兩篇寫了詞法分析和語法分析,比較偏向實踐。這一篇來看一下語言設計里一個比較重要的部分:名字。在大部分語言里,名字就是標識符,如果從抽象層面來看名字就是對更低一級的內存之類的概念的一層抽象。但是名字還有其它相關的比如它的約束時間和生存周期等等

    約束時間

    約束就是兩個東西之間的一種關聯,例如一個名字和它所命名的事物,約束時間就是指創建約束的時間。有關的約束可以在許多不同的時間作出

    • 語言設計時
    • 語言實現時
    • 編寫程序時
    • 編譯時
    • 鏈接時
    • 裝入時
    • 運行時

    這就是為什麼基於編譯的語言實現通常會比基於解釋器的語言的實現更高效的原因,因為基於編譯的語言在更早的時候就做了約束,比如對於全局變量在編譯時就已經確定了它在內存中的布局了

    對象生存期和存儲管理

    在名字和它們所引用的對象的約束之間有幾個關鍵事件

    • 對象的創建
    • 約束的創建
    • 對變量、子程序、類型等的引用,所有這些都使用了約束
    • 對可能暫時無法使用的約束進行失活或者重新約束
    • 約束的撤銷
    • 對象的撤銷

    對象的生存期和存儲分配機制有關

    • 靜態對象被賦予一個絕對地址,這個地址在程序的整個執行過程中都保持不變
    • 棧對象按照後進先出的方式分配和釋放,通常與子程序的調用和退出同時進行
    • 堆對象可以在任意時刻分配或者釋放,它們要求更通用的存儲管理算法

    靜態分配

    全局變量是靜態對象最顯而易見的例子,還有構成程序的機器語言翻譯結果的那些指令,也可以看作是靜態分配對象。

    還有像每次調用函數都會保持相同的值的局部變量也是靜態分配的。對於數值和字符串這些常量也是靜態分配。

    還有用來支持運行時的各種程序,比如廢料收集和異常處理等等也可以看作是靜態分配

    基於棧的分配

    如果一種語言允許遞歸,那麼局部變量就不能使用靜態分配的方式了,因為在同一時刻,一個局部變量存在的實例個數是不確定的

    所以一般對於子程序,都用棧來保存它相關的變量信息。在運行時,一個子程序的每個實例都在棧中有一個相應的棧幀,保存着它的參數、返回值、局部變量和一些簿記信息

    基於堆的分配

    堆是一塊存儲區域,其中的子存儲塊可以在任意時間分配與釋放。因為堆具有它的動態性,所以就需要對堆空間進行嚴格的管理。許多存儲管理算法都維護着堆中當前尚未使用的存儲塊的一個鏈接表,稱為自由表。

    初始時這個表只有一個塊,就是整個堆,每當遇到分配請求時,算法就在表中查找一個大小適當的塊。所以當請求次數增多,就會出現碎片問題,也需要相應的解決

    所以有廢料收集的語言其實就是對堆的管理

    作用域作用

    一個約束起作用的那一段程序正文區域,稱為這個約束的作用域。

    現在大多數語言使用的都是靜態作用域,也就是在編譯時就確定了。也有少數語言使用動態作用域,它們的約束需要等到運行時的執行流才能確定

    靜態作用域

    在使用靜態作用域的語言,也叫作詞法作用域。一般當前的約束就是程序中包圍着一個給定點的最近的,其中有與該名字匹配的聲明的那個快中建立的那個約束。比如C語言在進入子程序時,如果局部變量和全局變量,那麼當前的約束就是與局部變量關聯,直到退齣子程序才撤銷這個約束

    但是有的語言提供了一種可以提供約束的生存期的機制,比如Fortran的save和C的static

    嵌套子程序

    有許多語言允許一個子程序嵌套在另一個子程序的。這樣有關約束的定義通常來說都是首先用這個名字在當前、最內層的作用域中查找相應的聲明,如果找不到就直接到更外圍的作用域查找當前的約束,直到到達全局作用域,否則就發生一個錯誤

    訪問非局部變量

    上面提到的訪問外圍作用域的變量,但是當前子程序只能訪問到當前的棧幀,所以就需要一個調用幀鏈來讓當前的作用域訪問到外圍作用,通過調用順序形成一個靜態鏈

    聲明的順序

    關於約束還有一個問題,就是在同一作用域里,先聲明的名字是否能使用在此之後的聲明

    在Pascal里有這樣兩條規則:

    1. 修改變量要求名字在使用之前就進行聲明
    2. 但是當前聲明的作用域是整個程序塊

    所以在這兩個的相互作用下,會造成一個讓人吃驚的問題

    const N = 10;
    
    procedure foo;
    const
      M = N; (*靜態語義錯誤*)
      N = 20;

    但是在C、C++和Java等語言就不會出現這個問題,它們都規定標識符的作用域不是整個塊,而是從其聲明到塊結束的那一部分

    並且C++和Java還進一步放寬了規則,免除了使用之前必須聲明的要求

    模塊

    恰當模塊化的代碼可以減少程序員的思維負擔,因為它最大限度的減少了理解系統的任意給定部分時所需的信息量。在設計良好的程序中,模塊之間的接口應盡可能的小,所有可能改變的設計決策都隱藏在某個模塊里。

    模塊作為抽象

    模塊可以將一組對象(如子程序、變量、類型)封裝起來。使得:

    1. 這些內部的對象相互可見
    2. 但是外部對象和內部對象,除非显示的導入,否則都是不可見的

    模塊作為管理器

    模塊使我們很容易的創建各種抽象,但是如果需要多個棧的實例,那麼就需要一個讓模塊成為一個類型的管理器。這種管理器組織方式一般都是要求在模塊中增加創建/初始化函數,並給每一個函數增加一個用於描述被操作的實例

    模塊類型

    對於像這種多實例的問題,除了管理器,在許多語言里的解決方法都是可以將模塊看作是類型。當模塊是類型的時候,就可以將當前的方法認為是屬於這個類型的,簡單來說就是調用方法變化了

    push(A, x) -> A.push(x)

    本質上的實現區別不大

    面向對象

    在更面向對象里的方法里,可以把類看作是一種擴充了一種繼承機制的模塊類型。繼承機制鼓勵其中所有操作都被看作是從屬於對象的,並且新的對象可以從現有對象繼承大部分的操作,而不需要為這些操作重寫代碼。

    類的概念最早應該是起源於Simula-67,像後來的C++,Java和C#中的類的思想也都起源於它。類也是像Python和Ruby這些腳本語言的核心概念

    從模塊到模塊類型再到類都是有其思想基礎,但是最初都是為了更好的數據抽象。但是即使有了類也不能完全取代模塊,所以許多語言都提供了面向對象和模塊的機制

    動態作用域

    在使用動態作用域的語言中,名字與對象間的約束依賴於運行時的控制流,特別是依賴子程序的調用順序

    n : integer
    
    procedure first
      n := 1
    
    procedure second
      n : integer
      first()
    
    n := 2
    if read_integer() > 0
      second()
    else
      first()
    write_integer()

    這裏最後的輸出結果完全取決於read_integer讀入的数字的正負,如果為正,輸出就為2,否則就打印一個1

    作用域的實現

    為了跟蹤靜態作用域程序中的哥哥名字,編譯器需要依靠一個叫做符號表的數據結構。從本質上看,符號表就是一個記錄名字和它已知信息的映射關係的字典,但是由於作用域規則,所以還需要更強大的數據結構。像之前那個寫編譯器系列的符號表就是使用哈希表加上同一層作用域鏈表來實現的

    而對於動態作用域來說就需要在運行時執行一些操作

    作用域中名字的含義

    別名

    在基於指針的數據結構使用別名是很自然的情況,但是使用別名可能會導致編譯器難以優化或者造成像懸空引用的問題,所以需要謹慎使用

    重載

    在大多數語言中都或多或少的提供了重載機制,比如C語言中(+)可以被用在整數類型也可以用在浮點數類型,還有Java中的String類型也支持(+)運算髮

    要在編譯器的符號表中處理重載問題,就需要安排查找程序根據當前的上下文環境返回一個有意義的符號

    比如C++、Java和C#中的類方法重載都可以根據當前的參數類型和數量來判斷使用哪個符號

    內部運算符的重載

    C++、C#和Haskell都支持用戶定義的類型重載內部的算術運算符,在C++和C#的內部實現中通常是將A+B看作是operator+(A, B)的語法糖

    多態性

    對於名字,除了重載還有兩個重要的概念:強制和多態。這三個概念都用於在某些環境中將不同類型的參數傳給一個特定名字的子程序

    強制是編譯器為了滿足外圍環境要求,自動將某類型轉換為另一類型的值的操作

    所以在C中,定義一個計算整數或者浮點數兩個值中的最小值的函數

    double min(double x, double y);

    只要浮點數至少有整數那麼多有效二進制位,那麼結果就一定會是正確的。因為編譯器會對int類型強制轉換為double類型

    這是強制提供的方法,但是多態性提供的是,它使同一個子程序可以不加轉換的接受多種類型的參數。要使這個概念有意義,那麼這多種類型肯定要具有共同的特性

    顯式的參數多態性就叫做泛型,像Ada、C++、Clu、Java和C#都支持泛型機制,像剛才的例子就可以在Ada中用泛型來實現

    generic
      type T is private;
      with function "<" (x, y : T) return Boolean;
    function min(x, y : T) return T;
    
    function min(x, y : T) return T is
    begin
      if x < y then return x;
      else return y;
      end if;
    end min
    
    function string_min is new min(string, "<")
    function date_min is new min(date, date_precedes);

    像List和ML中就可以直接寫

    (define min (lambda (a b) (if (< a b) a b)))

    其中有關類型的任何細節都由解釋器處理

    引用環境的約束

    提到引用環境的約束就有兩種方式:淺約束和深約束

    推遲到調用時建立約束的方式淺約束。一般動態作用域的語言默認是淺約束,當然動態作用域和深約束也是可以組合到一起的。
    執行時依然使用傳遞時的引用環境,而非執行時的引用環境。那麼這種規則稱為深約束,一般靜態作用域的語言默認是深約束

    閉包

    為了實現神約束,需要創建引用環境的一種顯示錶示形式,並將它與對有關子程序的引用捆綁在一起,這樣的捆綁叫做閉包

    總而言之,如果子程序可以被當作參數傳遞,那麼它的引用環境一樣也會被傳遞過去

    一級值和非受限生存期

    一般而言,在語言中,如果一個值可以賦值給變量、可以當作參數傳遞、可以從子程序返回,那麼它被稱為具有一級狀態(和我們在js中說函數是一等公民一個含義)。大多數的語言中數據對象都是一級狀態。二級狀態是只能當作參數傳遞;三級值則是連參數也不能做,比如C#中一些+-*/等符號。

    在一級子程序會出現一個複雜性,就是它的生存期可能持續到這個子程序的作用域的執行期外。為了避免這一問題,大部分函數式語言都表示局部變量具有非受限的生命周期,它們的生命周期無限延長,直到GC能證明這些對象再也不使用了才會撤銷。那麼不撤銷帶來的問題就是這些子程序的存儲分配基於棧幀是不行了,只能是基於堆來分配管理。為了維持能基於棧的分配,有些語言會限制一級子程序的能力,比如C++,C#,都是不允許子程序嵌套,也就從根本上不會存在閉包帶來的懸空引用問題。

    小結

    這一篇從名字入手,介紹了名字與其背後的對象的約束關係、以及約束時間的概念;然後介紹了對象的分配策咯(靜態、棧、堆);緊接着討論了名字與對象之間建立的約束的生命周期,並由此引出了作用域的概念;進一步延伸出多個約束組成的引用環境的相關概念以及問題。

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

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

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

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

  • 從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十二Swagger(參數)使用二

    從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十二Swagger(參數)使用二

      引言

      在 中提到了 Swagger 的基本使用,僅限於沒有參數,沒有驗證的那種api文檔生成,那麼這篇就連接上篇繼續,在一般具有安全性、權限等驗證的接口上,

      都會在header/url中加上請求者的秘鑰、簽名等,當然也有可能添加到body等其它地方, Swashbuckle.AspNetCore 都支持這些寫法。

      如何使用 — 下面將介紹兩種使用方式

    兩種方式參數設置到何處都是在  In屬性上,屬性對於值如下:    參考

    • query: 參数字段值對應放在url中
    • header: 參數值對應放在header param中
    • body: 參數對應放到請求體中
    • path: 參數應該對應放到請求路徑  // 具體貌似沒用
    • formData: 參數對應放到請求表單中

      第一種:將一個或多個參數保護API的“securityDefinitions”添加到生成的Swagger中。

    這種是直接在文檔的右上方添加一個 Authorize 按鈕,設置了值后,每一個請求都會在設置的位置上加上相應的值,在 上一篇隨筆中的 ConfigureServices 方法中,

    對應位置 services.AddSwaggerGen(options =>{}) 中的  XmlComments  下 添加代碼如下:

                    options.AddSecurityDefinition("token", new ApiKeyScheme
                    {
                        Description = "token format : {token}",//參數描述
                        Name = "token",//名字
                        In = "header",//對應位置
                        Type = "apiKey"//類型描述
                    });
                    options.AddSecurityDefinition("sid", new ApiKeyScheme
                    {
                        Description = "sid format : {sid}",//參數描述
                        Name = "sid",//名字
                        In = "header",//對應位置
                        Type = "apiKey"//類型描述
                    });
                    //添加Jwt驗證設置 設置為全局的,不然在代碼中取不到
                    options.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>> {
                        { "token", Enumerable.Empty<string>() },
                        { "sid", Enumerable.Empty<string>() },
                    });

      添加完成后,運行起來看下效果,效果圖: 

     設置上對應值,調用測試方法,可以在header中取到剛設置到的值,

     這裡能看到,可以取到設置的參數了。這樣一來,在需要驗證的接口上,我們就可以通過接口文檔來測試了。基本不用再藉助postman等接口測試工具了。

    但是,但是,這裡有一個問題,就是只要設置了參數值,每一次訪問都會在請求中帶上參數。

    下面將介紹第二種方式,只給需要驗證用戶的接口上添加驗證參數。

      第二種:使用“filters”擴展Swagger生成器,來實現只在需要添加參數的方法上添加參數。複雜的可以根據自己的需求來添加對應參數

    實現方式就是先新建一個類,名: SwaggerParameter ,實現 IOperationFilter 接口。SwaggerParameter 類代碼如下: 

        /// <summary>
        /// 自定義添加參數
        /// </summary>
        public class SwaggerParameter : IOperationFilter
        {
            /// <summary>
            /// 實現 Apply 方法
            /// </summary>
            /// <param name="operation"></param>
            /// <param name="context"></param>
            public void Apply(Operation operation, OperationFilterContext context)
            {
                if (operation.Parameters == null) operation.Parameters = new List<IParameter>();
                var attrs = context.ApiDescription.ActionDescriptor.AttributeRouteInfo;
                var t = typeof(BaseUserController);
                //先判斷是否是繼承用戶驗證類
                if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor descriptor && context.MethodInfo.DeclaringType?.IsSubclassOf(t) == true)
                {
                    //再驗證是否允許匿名訪問
                    var actionAttributes = descriptor.MethodInfo.GetCustomAttributes(inherit: true);
                    bool isAnonymous = actionAttributes.Any(a => a is AllowAnonymousAttribute);
                    // 需要驗證的方法添加
                    if (!isAnonymous)
                    {
                        operation.Parameters.Add(new NonBodyParameter()
                        {
                            Name = "sid",
                            In = "header", //query header body path formData
                            Type = "string",
                            Required = true,//是否必選
                            Description = "登錄返回的sid"
                        });
                        operation.Parameters.Add(new NonBodyParameter()
                        {
                            Name = "token",
                            In = "header", //query header body path formData
                            Type = "string",
                            Required = true,//是否必選
                            Description = "登錄返回的token"
                        });
                    }
                }
            }
        }

     運行起來后,進入到  文檔頁面,可以看到右上角的 Authorize 按鈕已經不見了,在不需要驗證的方法上,也找不到相應需要設置參數的輸入框。就只有在需要驗證的接口上才有。

    參考Swagger文檔圖如下: 

    參考代碼圖如下:

     

    效果圖: 

      這樣一來設置也就完成了。從上面就能看出,就只有需要用戶驗證的接口才會有相應參數。 

     

    我的設置方式是先定義了用戶驗證控制器類,讓需要用戶驗證的控制器繼承該控制器,然後在該控制器中不需要用戶驗證的接口上加上 AllowAnonymous 屬性 

    設置fitter時就可以根據上面提到的兩個點來進行判斷是否需要加上參數,如果不是這樣實現的,可以根據自己的需求變更fitter類,來控制文檔的生成。 

     

    以上若有什麼不對或可以改進的地方,望各位指出或提出意見,一起探討學習~ 

    有需要源碼的可通過此 鏈接拉取 覺得還可以的給個 start 和點個 下方的推薦哦~~謝謝!

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

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

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

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

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

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

  • 【algo&ds】8.最小生成樹

    【algo&ds】8.最小生成樹

    1.最小生成樹介紹

    什麼是最小生成樹?

    最小生成樹(Minimum spanning tree,MST)是在一個給定的無向圖G(V,E)中求一棵樹T,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權值和最小。

    2.prim算法

    和Dijkstra算法很像!!請看如下Gif圖,prim算法的核心思想是對圖G(V,E)設置集合S,存放已被訪問的頂點,然後每次從集合V-S中選擇與集合S的最短距離最小的一個頂點(記為u),訪問並加入集合S。之後,令頂點u為中間點,優化所有從u能到達的頂點v與集合s之間的最短距離。這樣的操作執行n次,直到集合s中包含所有頂點。

    不同的是,Dijkstra算法中的dist是從源點s到頂點w的最短路徑;而prim算法中的dist是從集合S到頂點w的最短路徑,以下是他們的偽碼描述對比,關於Dijkstra算法的詳細描述請

    算法實現:

    #include<iostream>
    #include<vector>
    #define INF 100000
    #define MaxVertex 105
    typedef int Vertex; 
    int G[MaxVertex][MaxVertex];
    int parent[MaxVertex];   // 並查集 
    int dist[MaxVertex]; // 距離 
    int Nv;    // 結點 
    int Ne;    // 邊 
    int sum;  // 權重和 
    using namespace std; 
    vector<Vertex> MST;  // 最小生成樹 
    
    // 初始化圖信息 
    void build(){
        Vertex v1,v2;
        int w;
        cin>>Nv>>Ne;
        for(int i=1;i<=Nv;i++){
            for(int j=1;j<=Nv;j++)
                G[i][j] = 0;  // 初始化圖 
            dist[i] = INF;   // 初始化距離
            parent[i] = -1;  // 初始化並查集 
        }
        // 初始化點
        for(int i=0;i<Ne;i++){
            cin>>v1>>v2>>w;
            G[v1][v2] = w;
            G[v2][v1] = w;
        }
    }
    
    // Prim算法前的初始化 
    void IniPrim(Vertex s){
        dist[s] = 0;
        MST.push_back(s);
        for(Vertex i =1;i<=Nv;i++)
            if(G[s][i]){
                dist[i] = G[s][i];
                parent[i] = s;
            } 
    }
    
    // 查找未收錄中dist最小的點 
    Vertex FindMin(){
        int min = INF;
        Vertex xb = -1;
        for(Vertex i=1;i<=Nv;i++)
            if(dist[i] && dist[i] < min){ 
                min = dist[i];
                xb = i;
            }
        return xb;
    }
    
    void output(){
        cout<<"被收錄順序:"<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<MST[i]<<" ";
        cout<<"權重和為:"<<sum<<endl; 
        cout<<"該生成樹為:"<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<parent[i]<<" ";
    }
    
    void Prim(Vertex s){
        IniPrim(s);
        while(1){
            Vertex v = FindMin();
            if(v == -1)
                break;
            sum += dist[v];
            dist[v] = 0;
            MST.push_back(v);
            for(Vertex w=1;w<=Nv;w++)
                if(G[v][w] && dist[w])
                    if(G[v][w] < dist[w]){
                        dist[w] = G[v][w];
                        parent[w] = v;
                    }
        }
    }
    
    
    int main(){
        build();
        Prim(1);
        output();
        return 0;
    } 

    關於prim算法的更加詳細講解請

    3.kruskal算法

    Kruskal算法也可以用來解決最小生成樹的問題,其算法思想很容易理解,典型的邊貪心,其算法思想為:

    • 在初始狀態時隱去圖中所有的邊,這樣圖中每個頂點都是一個單獨的連通塊,一共有n個連通塊
    • 對所有邊按邊權從小到大進行排序
    • 按邊權從小到大測試所有邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊中,則把這條測試邊加入當前最小生成樹中,否則,將邊捨棄。
    • 重複執行上一步驟,直到最小生成樹中的邊數等於總頂點數減一 或者測試完所有邊時結束;如果結束時,最小生成樹的邊數小於總頂點數減一,說明該圖不連通。

    請看下面的Gif圖!

    算法實現:

    #include<iostream>
    #include<string>
    #include<vector>
    #include<queue>
    #define INF 100000
    #define MaxVertex 105
    typedef int Vertex; 
    int G[MaxVertex][MaxVertex];
    int parent[MaxVertex];   // 並查集最小生成樹 
    int Nv;    // 結點 
    int Ne;    // 邊 
    int sum;  // 權重和 
    using namespace std; 
    struct Node{
        Vertex v1;
        Vertex v2;
        int weight; // 權重 
        // 重載運算符成最大堆 
        bool operator < (const Node &a) const
        {
            return weight>a.weight;
        }
    };
    vector<Node> MST;  // 最小生成樹 
    priority_queue<Node> q;   // 最小堆 
    
    // 初始化圖信息 
    void build(){
        Vertex v1,v2;
        int w;
        cin>>Nv>>Ne;
        for(int i=1;i<=Nv;i++){
            for(int j=1;j<=Nv;j++)
                G[i][j] = 0;  // 初始化圖
            parent[i] = -1;
        }
        // 初始化點
        for(int i=0;i<Ne;i++){
            cin>>v1>>v2>>w;
            struct Node tmpE;
            tmpE.v1 = v1;
            tmpE.v2 = v2;
            tmpE.weight = w;
            q.push(tmpE); 
        }
    }
    
    //  路徑壓縮查找 
    int Find(int x){
        if(parent[x] < 0)
            return x;
        else
            return parent[x] = Find(parent[x]);
    } 
    
    //  按秩歸併 
    void Union(int x1,int x2){
        if(parent[x1] < parent[x2]){
            parent[x1] += parent[x2];
            parent[x2] = x1;
        }else{
            parent[x2] += parent[x1];
            parent[x1] = x2;
        }
    } 
    
    void Kruskal(){
        // 最小生成樹的邊不到 Nv-1 條且還有邊 
        while(MST.size()!= Nv-1 && !q.empty()){
            Node E = q.top();  // 從最小堆取出一條權重最小的邊
            q.pop(); // 出隊這條邊 
            if(Find(E.v1) != Find(E.v2)){  // 檢測兩條邊是否在同一集合 
                sum += E.weight; 
                Union(E.v1,E.v2);     // 並起來 
                MST.push_back(E);
            }
        }
        
    } 
    
    
    void output(){
        cout<<"被收錄順序:"<<endl; 
        for(Vertex i=0;i<Nv;i++)
            cout<<MST[i].weight<<" ";
        cout<<"權重和為:"<<sum<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<parent[i]<<" ";
        cout<<endl;
    }
    
    
    int main(){
        build();
        Kruskal();
        output();
        return 0;
    } 

    關於kruskal算法更詳細的講解

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

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

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

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

  • pod刪除主要流程源碼解析

    本文以v1.12版本進行分析

    當一個pod刪除時,client端向apiserver發送請求,apiserver將pod的deletionTimestamp打上時間。kubelet watch到該事件,開始處理。

    syncLoop

    kubelet對pod的處理主要都是在syncLoop中處理的。

    func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
    for {
    ...
            if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) {
                break
            }
    ...

    與pod刪除主要在syncLoopIteration中需要關注的是以下這兩個。

    func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
        syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
        select {
        case u, open := <-configCh:
    ...
            switch u.Op {
    ...
            case kubetypes.UPDATE:
                handler.HandlePodUpdates(u.Pods)
    ...
        case <-housekeepingCh:
            if !kl.sourcesReady.AllReady() {
            } else {
                if err := handler.HandlePodCleanups(); err != nil {
                    glog.Errorf("Failed cleaning pods: %v", err)
                }
            }
        }

    第一個是由於發送給apiserver的DELETE請求觸發的,增加了deletionTimestamp的事件。這裏對應於kubetypes.UPDATE。也就是會走到HandlePodUpdates函數。

    另外一個與delete相關的是每2s執行一次的來自於housekeepingCh的定時事件,用於清理pod,執行的是handler.HandlePodCleanups函數。這兩個作用不同,下面分別進行介紹。

    HandlePodUpdates

    先看HandlePodUpdates這個流程。只要打上了deletionTimestamp,就必然走到這個流程里去。

    func (kl *Kubelet) HandlePodUpdates(pods []*v1.Pod) {
        for _, pod := range pods {
    ...
            kl.dispatchWork(pod, kubetypes.SyncPodUpdate, mirrorPod, start)
        }
    }

    在HandlePodUpdates中,進而將pod的信息傳遞到dispatchWork中處理。

    func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType, mirrorPod *v1.Pod, start time.Time) {
        if kl.podIsTerminated(pod) {
            if pod.DeletionTimestamp != nil {
                kl.statusManager.TerminatePod(pod)
            }
            return
        }
        // Run the sync in an async worker.
        kl.podWorkers.UpdatePod(&UpdatePodOptions{
            Pod:        pod,
            MirrorPod:  mirrorPod,
            UpdateType: syncType,
            OnCompleteFunc: func(err error) {
    ...

    這裏首先通過判斷了kl.podIsTerminated(pod)判斷pod是不是已經處於了Terminated狀態。如果是的話,則不進行下面的kl.podWorkers.UpdatePod。

    func (kl *Kubelet) podIsTerminated(pod *v1.Pod) bool {
        status, ok := kl.statusManager.GetPodStatus(pod.UID)
        if !ok {
            status = pod.Status
        }
        return status.Phase == v1.PodFailed || status.Phase == v1.PodSucceeded || (pod.DeletionTimestamp != nil && notRunning(status.ContainerStatuses))
    }

    這個地方特別值得注意的是,並不是由了DeletionTimestamp就會認為是Terminated狀態,而是有DeletionTimestamp且所有的容器不在運行了。也就是說如果是一個正在正常運行的pod,是也會走到kl.podWorkers.UpdatePod中的。UpdatePod通過一系列函數調用,最終會通過異步的方式執行syncPod函數中進入到syncPod函數中。

    func (kl *Kubelet) syncPod(o syncPodOptions) error {
    ...
        if !runnable.Admit || pod.DeletionTimestamp != nil || apiPodStatus.Phase == v1.PodFailed {
            var syncErr error
            if err := kl.killPod(pod, nil, podStatus, nil); err != nil {
    ...

    在syncPod中,調用killPod從而對pod進行停止操作。

    killPod

    killPod是停止pod的主體。在很多地方都會使用。這裏主要介紹下起主要的工作流程。停止pod的過程主要發生在killPodWithSyncResult函數中。

    func (m *kubeGenericRuntimeManager) killPodWithSyncResult(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (result kubecontainer.PodSyncResult) {
        killContainerResults := m.killContainersWithSyncResult(pod, runningPod, gracePeriodOverride)
    ...
        for _, podSandbox := range runningPod.Sandboxes {
                if err := m.runtimeService.StopPodSandbox(podSandbox.ID.ID); err != nil {
    ...

    killPodWithSyncResult的主要工作分為兩個部分。killContainersWithSyncResult負責將pod中的container停止掉,在停止后再執行StopPodSandbox。

    func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, reason string, gracePeriodOverride *int64) error {
        if err := m.internalLifecycle.PreStopContainer(containerID.ID); err != nil {
            return err
        }
    ...
        err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)

    killContainersWithSyncResult的主要工作是在killContainer中完成的,這裏可以看到,其中的主要兩個步驟是在容器中進行prestop的操作。待其成功后,進行container的stop工作。至此所有的應用容器都已經停止了。下一步是停止pause容器。而StopPodSandbox就是執行這一過程的。將sandbox,也就是pause容器停止掉。StopPodSandbox是在dockershim中執行的。

    func (ds *dockerService) StopPodSandbox(ctx context.Context, r *runtimeapi.StopPodSandboxRequest) (*runtimeapi.StopPodSandboxResponse, error) {
    ...
    if !hostNetwork && (ready || !ok) {
    ...
            err := ds.network.TearDownPod(namespace, name, cID, annotations)
    ...
        }
        if err := ds.client.StopContainer(podSandboxID, defaultSandboxGracePeriod); err != nil {

    StopPodSandbox中主要的部分是先進行網絡卸載,再停止相應的容器。在完成StopPodSandbox后,至此pod的所有容器都已經停止完成。至於volume的卸載,是在volumeManager中進行的。本文不做單獨介紹了。停止后的容器在pod徹底清理后,會被gc回收。這裏也不展開講了。

    HandlePodCleanups

    上面這個流程並不是刪除流程的全部。一個典型的情況就是,如果container都不是running,那麼在UpdatePod的時候都return了,那麼又由誰來處理呢?這裏我們回到最開始,就是那個每2s執行一次的HandlePodCleanups的流程。也就是說比如container處於crash,container正好不是running等情況,其實是在這個流程里進行處理的。當然HandlePodCleanups的作用不僅僅是清理not running的pod,再比如數據已經在apiserver中強制清理掉了,或者由於其他原因這個節點上還有一些沒有完成清理的pod,都是在這個流程中進行處理。

    func (kl *Kubelet) HandlePodCleanups() error {
    ... 
        for _, pod := range runningPods {
            if _, found := desiredPods[pod.ID]; !found {
                kl.podKillingCh <- &kubecontainer.PodPair{APIPod: nil, RunningPod: pod}
            }
        }

    runningPods是從cache中獲取節點現有的pod,而desiredPods則是節點上應該存在未被停止的pod。如果存在runningPods中有而desiredPods中沒有的pod,那麼它應該被停止,所以發送到podKillingCh中。

    func (kl *Kubelet) podKiller() {
    ...
        for podPair := range kl.podKillingCh {
    ...
    
            if !exists {
                go func(apiPod *v1.Pod, runningPod *kubecontainer.Pod) {
                    glog.V(2).Infof("Killing unwanted pod %q", runningPod.Name)
                    err := kl.killPod(apiPod, runningPod, nil, nil)
    ...
                }(apiPod, runningPod)
            }
        }
    }

    在podKiller流程中,會去接收來自podKillingCh的消息,從而執行killPod,上文已經做了該函數的介紹了。

    statusManager

    在最後,statusManager中的syncPod流程,將會進行檢測,通過canBeDeleted確認是否所有的容器關閉了,volume卸載了,cgroup清理了等等。如果這些全部完成了,則從apiserver中將pod信息徹底刪除。

    func (m *manager) syncPod(uid types.UID, status versionedPodStatus) {
    ...
        if m.canBeDeleted(pod, status.status) {
            deleteOptions := metav1.NewDeleteOptions(0)
            deleteOptions.Preconditions = metav1.NewUIDPreconditions(string(pod.UID))
            err = m.kubeClient.CoreV1().Pods(pod.Namespace).Delete(pod.Name, deleteOptions)
    ...

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

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

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

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

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

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

  • 網絡相關的命令工具研究報告

    網絡相關的命令工具研究報告

    主機配置:DHCP

      DHCP(動態主機配置協議),是在一台主機啟動后,第一個運行的客戶/服務器應用程序。換言之,當一台主機啟動后,如果它認為自己當前應當連接到因特網上,但又不知道自己的IP地址時,DHCP就以引導程序的身份發揮作用。

      每個連接到TCP/IP互聯網的計算機都必須知道自己的IP地址、一個路由器的IP地址、一個名字服務器的IP地址以及自己的子網掩碼這四種信息。

     DHCP分組格式:

     

     

    一、曾經使用過的協議

      在DHCP成為正式的主機配置協議之前,還有過一些其他的協議。

    1.RARP:

      在因特網時代的初期,人們曾設計了一個稱為逆地址解析協議(Reverse Address Resolution Protocol,RARP)來向被引導的主機提供IP地址。實際上,RARP是ARP的一個版本。ARP將一個IP地址映射為一個物理地址,而RARP則將一個物理地址映射成為一個IP地址。但是RARP已經被淘汰了,原因有兩個:首先,RARP利用了數據鏈路層的廣播服務,這也就表示每個網絡上都必須存在一台RARP服務器。第二,RARP只能提供計算機的IP地址,但如今的計算機需要前面提到的所有四種信息。

    2.BOOTP:

      引導程序協議(BOOTstrap Protocol,BOOTP)是DHCP的先驅。它是一個客戶/服務器協議,被設計用來克服RARP協議存在的缺陷。但是BOOTP是一個靜態配置協議,當客戶請求自己的IP地址時,BOOTP服務器就諮詢一張表,將客戶的物理地址映射成相應的IP地址。這就意味着客戶的物理地址和IP地址之間的綁定是已經存在的。這個綁定關係是事先設定好的。

      在某些場合,我們需要的是一個動態配置協議。例如,當一台主機從一個物理網絡移動到另一個物理網絡時,它的物理地址就改變了。再比如,有時候主機需要在某一段時間內使用一個臨時的IP地址。BOOTP無法處理這種狀況,因為物理地址和IP地址之間的綁定是靜態的,是固定存放在一張表中的,除非管理員更改這張表。

      而DHCP的設計就是為了解決這些不足之處。

    3. DHCP:

      動態主機配置協議(Dynamic Host Configuration Protocol,DHCP)是一種客戶/服務器協議,設計這個協議是為了將上述四種信息傳遞給無盤計算機或者第一次啟動的計算機。DHCP是BOOTP的繼承者,並且能夠兼容BOOTP。 

    二、DHCP操作

      DHCP客戶和DHCP服務器可以在同一個網絡上,也可以位於不同的網絡。

    1.DHCP客戶和DHCP服務器在同一個網絡

      雖然這種情況不是很常見,不過管理員可以把客戶和服務器放在同一個網絡中。如圖所示:

     

    這種情況的操作如下:

    (1)DHCP服務器在UDP端口67發出被動打開命令,等待客戶請求。

    (2)被引導的客戶在UDP端口68發出主動打開命令。這個報文被封裝成UDP用戶報,其目的端口是67,源端口號是68。這個UDP用戶數據報在封裝成IP數據包。客戶使用的是全0的源地址和全1的目的地址。

    (3)服務器或者用廣播報文,或者用單播報文來響應這個用戶,它使用了UDP源端口號67和目的端口68.這個響應可以是單播的,因為服務器知道客戶的IP地址,同時也知道客戶的物理地址,也就是說它不需要使用ARP的服務進行從邏輯地址到物理地址的映射。但是某些系統不允許旁路掉ARP,結果就要使用廣播地址。

    2. DHCP客戶和DHCP服務器在不同的網絡

    如圖所示:

     
      像其他應用層的進程一樣,客戶可以在某個網絡上,而服務器可以在相隔好幾個網絡之外的另一網絡上。這就帶來了一個必須要解決的問題。DHCP請求是廣播發送的,因為客戶不知道服務器的IP地址。而廣播的IP數據報不能通過任何路由器。路由器收到這樣的分組就丟棄它。

      要解決這個問題,就需要一个中介物。某台主機(或是一台能夠配置為在應用層工作的路由器)可以用來充當中繼。在這種情況下,該主機就稱為中繼代理。中繼代理知道DHCP服務器的單播地址,並在端口67監聽廣播報文。當它收到這種類型的分組后,就把它封裝成一個單播數據報,並且把此請求發送給DHCP服務器。攜帶了單播目的地址的分組可以被任何一個路由器轉發,最終到達DHCP服務器。DHCP服務器知道這個報文來自中繼代理,因為在請求報文中有一個字段定義了中繼代理的IP地址。中繼代理在收到回答后,再把它發送給DHCP客戶。 

    三、配置

      人們設計DHCP是為了提供靜態和動態的地址分配。

    1.靜態地址分配

      對於靜態地址分配,DHCP有一個專門的數據庫,可以靜態地吧物理地址綁定到IP地址。

    2.動態地址分配

      DHCP還有第二個數據庫,包括一個可用的IP地址池。第二個數據庫使DHCP成為動態的。當DHCP客戶請求臨時的IP地址時,DHCP服務器就從可用(即為使用的)IP地址池中取出一個IP地址進行指派,這個IP地址的使用時間長短可協商。

      當DHCP客戶想DHCP服務器發送請求是,服務器首先檢查它的靜態數據庫。若靜態數據庫中存在所請求物理地址的表項,則返回給這個客戶的永久IP地址。反之,若靜態數據庫中沒有這個表項,服務器就從可用IP地址池中選擇一個IP地址,並把這個地址指派給客戶,然後再把相應的表項加入到動態數據庫中。

      如果主機要從一個網絡移動到另一個網絡,或者與一個網絡時連時斷,那麼DHCP的這種動態特性就有了用武之地。DHCP可以在有限時間內提供一個臨時的IP地址。

    從地址池指派的地址都是臨時地址。DHCP服務器向客戶授予某一段時間內對該地址池的租用權。當租用時效過期,客戶或者停止使用這個IP地址,或者續租。服務器有權力選擇同意或不同意續租。若服務器不同意,客戶就停止使用這個地址。

    3.轉換狀態

      為了提供動態的地址分配,DHCP客戶可以像狀態機那樣從一個狀態轉換到另一個狀態,狀態轉換取決於收到的報文和發送的報文。在這種情況下,報文的類型是由包含在DHCP分組中的標記為53的選項來定義的。標記為53 的選項如圖所示:

     

    DHCP的不同狀態:

    (1)INIT狀態

      當DHCP客戶首次啟動時,它處於INIT狀態(初始化狀態)。客戶使用端口67廣播DHCPDISCOVER報文(一個帶有DHCPDISCOVER選項的請求報文)。

    (2)SELECTING狀態

      在發送DHCPDISCOVER報文後,客戶就進入SELECTING(選擇)狀態。能夠提供這種類型服務的服務器要用DHCPOFFER報文進行相應。在此類報文中,服務器提供了一個IP地址。它們還要提供租用時間長度,其默認值是1小時。在發送DHCPOFFER報文的服務器,把提供的IP地址鎖定,使這個地址不會再提供給任何其他的客戶。客戶選擇所提供的地址中的一個,並向所選擇的服務器發送DHCPREQUEST報文。然後就進入REQUESTING狀態。如果客戶沒有收到DHCPOFFER報文,它還要再嘗試四次,每一次間隔2秒。如果對這些DHCPDISCOVER都沒有收到回答,客戶就睡眠5分鐘后再試。

    (3)REQUESTING狀態

      客戶保持在這個REQUESTING(請求)狀態,直至它收到來自服務器的DHCPACK報文為止,這個報文創建了客戶物理地址和它的IP地址之間的綁定。客戶收到DHCPACK報文後進入BOUND狀態。

    (4)BOUND狀態

      在這種狀態下,客戶可以使用該IP地址,直到租用時間到期。當到達租用時間的50%時,客戶就再發送一個DHCPREQUEST報文以請求更新。於是,客戶進入RENEWING。在BOUND(綁定)狀態時,客戶也可以取消租用,並進入到初始化狀態。

    (5)RENEWING狀態

      客戶保持在RENEWING(更新)狀態,直至下面兩個事件之一發生。客戶可以收到更新租用協定的DHCPACK報文。在這種情況下,客戶把計時器複位,然後回到BOUND狀態。或者,如果沒有收到DHCPACK,同時到達租用時間的87.5%時,客戶就進入REBINDING狀態。

    (6)REBINDING狀態

      客戶保持在REBINDING(重新綁定)狀態,直至下面三個事件之一發生。若客戶收到一個DHCPNACK報文或者租用時間到期,則回到初始化狀態,並嘗試得到另一個IP地址。若客戶收到DHCPACK報文,它就進入綁定狀態,並把計時器複位。

    DHCP不同狀態的轉換圖:

     

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

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

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

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

  • 氫燃料沒未來?Honda改拚電動車,明年搶攻歐陸

    氫燃料沒未來?Honda改拚電動車,明年搶攻歐陸

    本田汽車(Honda)計畫於2018年一次推出兩款全電動車,且宣告未來將以電動車作為發展主軸。

    根據《BusinessInsider》報導表示,本田六月就已表示將打造中國專屬電動車,8月29日發佈的聲明稿則指明另一款電動車是為歐洲客戶所設計。

    本田說,新城市電動概念車(Urban EV Concept)將在九月法蘭克福車展上亮相,這也是本田在歐洲第一款電動車,肩負打開歐洲電動車市場的任務。

    目前本田僅有一款純電動車在美國上市,不過本田表示,2030年旗下三分之二的車型都要電動化,達成目標的方法將在法蘭克福車展上對外說明。

    本田與日本同業豐田(Toyota)此前主要開發氫燃料車與混和動力車,但由上述可知本田策略已經轉向發展電動車,可能引領其它日本車廠跟進。

    (本文內容由授權使用。圖片出處:)

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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