ApplicationContextRunner如何簡化自動配置測試

 

1. 概覽

眾所周知,自動配置是Spring Boot的關鍵功能之一, 但測試自動配置可能會很棘手。

在以下部分中,我們將展示ApplicationContextRunner如何簡化自動配置測試。

2. 測試自動化配置方案

ApplicationContextRunner是一個實用程序類,它運行ApplicationContext並提供AssertJ樣式斷言。 最好用作測試類中的字段以便共享配置,然後我們在每個測試中進行自定義:

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

讓我們通過測試一些案例來展示它的魔力。

2.1. 測試Class Condition

在本節中,我們將測試一些使用@ConditionalOnClass和@ConditionalOnMissingClass 註解的自動配置類:

@Configuration
@ConditionalOnClass(ConditionalOnClassIntegrationTest.class)
protected static class ConditionalOnClassConfiguration {
    @Bean
    public String created() { return "This is created when ConditionalOnClassIntegrationTest is present on the classpath"; } } @Configuration @ConditionalOnMissingClass("com.baeldung.autoconfiguration.ConditionalOnClassIntegrationTest") protected static class ConditionalOnMissingClassConfiguration { @Bean public String missed() { return "This is missed when ConditionalOnClassIntegrationTest is present on the classpath"; } } 

我們想測試自動配置是否正確實例化或跳過createdmissing beans給定的預期條件。

  • ApplicationContextRunner為我們提供了withUserConfiguration方法,我們可以根據需要提供自動配置,以便為每個測試自定義ApplicationContext

  • run 方法將 ContextConsumer 作為將斷言應用於上下文的參數。 測試退出時,ApplicationContext將自動關閉:

@Test
public void whenDependentClassIsPresent_thenBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)       .run(context -> {         assertThat(context).hasBean("created");         assertThat(context.getBean("created"))           .isEqualTo("This is created when ConditionalOnClassIntegrationTest is present on the classpath");       }); }   @Test public void whenDependentClassIsPresent_thenBeanMissing() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)         .run(context -> {             assertThat(context).doesNotHaveBean("missed");         }); } 

通過前面的示例,我們發現測試classpath上存在某個類的場景的簡單性。但是,當類不在classpath上時,我們如何測試相反的情況呢

這就是FilteredClassLoader發揮作用的地方。它用於在運行時過濾classpath上指定的類:

@Test
public void whenDependentClassIsNotPresent_thenBeanMissing() {     this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)         .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))         .run((context) -> {             assertThat(context).doesNotHaveBean("created");             assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);         }); }   @Test public void whenDependentClassIsNotPresent_thenBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)       .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))       .run((context) -> {         assertThat(context).hasBean("missed");         assertThat(context).getBean("missed")           .isEqualTo("This is missed when ConditionalOnClassIntegrationTest is present on the classpath");         assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);       }); } 

2.2. 測試 Bean Condition

我們剛剛測試了 @ConditionalOnClass 和 @ConditionalOnMissingClass 註解, 現在 讓我們看看使用@ConditionalOnBean和@ConditionalOnMissingBean註釋時的情況。

首先, 我們同樣需要 一些自動配置的類:

@Configuration
protected static class BasicConfiguration {
    @Bean
    public String created() {         return "This is always created";     } } @Configuration @ConditionalOnBean(name = "created") protected static class ConditionalOnBeanConfiguration {     @Bean     public String createOnBean() {         return "This is created when bean (name=created) is present";     } } @Configuration @ConditionalOnMissingBean(name = "created") protected static class ConditionalOnMissingBeanConfiguration {     @Bean     public String createOnMissingBean() {         return "This is created when bean (name=created) is missing";     } } 

然後,我們將像上一節一樣調用withUserConfiguration方法,然後發送我們的自定義配置類來測試自動配置是否在不同的條件下恰當地實例化bean或跳過createOnBeancreateOnMissingBean :

@Test
public void whenDependentBeanIsPresent_thenConditionalBeanCreated() {     this.contextRunner.withUserConfiguration(BasicConfiguration.class,       ConditionalOnBeanConfiguration.class)     // ommitted for brevity } @Test public void whenDependentBeanIsNotPresent_thenConditionalMissingBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingBeanConfiguration.class)     // ommitted for brevity } 

2.3. 測試 Property Condition

在本節中,我們測試使用 @ConditionalOnPropertyannotations的自動配置類。

首先,我們需要這個測試的屬性:

com.baeldung.service=custom

然後,我們編寫嵌套的自動配置類,根據前面的屬性創建bean:

@Configuration
@TestPropertySource("classpath:ConditionalOnPropertyTest.properties") protected static class SimpleServiceConfiguration {     @Bean     @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "default")     @ConditionalOnMissingBean     public DefaultService defaultService() {         return new DefaultService();     }     @Bean @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "custom") @ConditionalOnMissingBean public CustomService customService() { return new CustomService(); } } 

現在,我們調用withPropertyValues方法來覆蓋每個測試中的屬性值:

@Test
public void whenGivenCustomPropertyValue_thenCustomServiceCreated() { this.contextRunner.withPropertyValues("com.baeldung.service=custom") .withUserConfiguration(SimpleServiceConfiguration.class) .run(context -> { assertThat(context).hasBean("customService"); SimpleService simpleService = context.getBean(CustomService.class); assertThat(simpleService.serve()).isEqualTo("Custom Service"); assertThat(context).doesNotHaveBean("defaultService"); }); } @Test public void whenGivenDefaultPropertyValue_thenDefaultServiceCreated() { this.contextRunner.withPropertyValues("com.baeldung.service=default") .withUserConfiguration(SimpleServiceConfiguration.class) .run(context -> { assertThat(context).hasBean("defaultService"); SimpleService simpleService = context.getBean(DefaultService.class); assertThat(simpleService.serve()).isEqualTo("Default Service"); assertThat(context).doesNotHaveBean("customService"); }); } 

3. 結論

總結一下, 這篇教程主要展示 如何使用ApplicationContextRunner運行帶有自定義的ApplicationContext並應用斷言.

我們在這裏介紹了最常用的場景,而不是列出如何自定義ApplicationContext 。

在此期間,請記住ApplicationConetxtRunner適用於非Web應用程序,因此請考慮WebApplicationContextRunner用於基於servlet的Web應用程序,ReactiveWebApplicationContextRunner用於響應式Web應用程序。

本文源代碼,請訪問GitHub。

原文:www.baeldung.com/spring-boot…

作者:baeldung

譯者:Leesen

 

【精選推薦文章】

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

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

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

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

Spring 中的Null-Safety

之前一直在某些代碼中看到過使用@Nullable 標註過的註釋,當時也沒有在意到底是什麼意思,所以這篇文章來談談Spring中關於Null的那些事。

在Java中不允許讓你使用類型表示其null的安全性,但Spring Framework 現在在org.sprinngframework.lang包提供以下註釋,以便聲明API和字段的可空性:

  • @Nullable: 用於指定參數、返回值或者字段可以作為null的註釋。
  • @NonNull: 與上述註釋相反,表明指定參數、返回值或者字段不允許為null。(不需要@NonNullApi和@NonNullFields適用的參數/返回值和字段)
  • @NonNullApi: 包級別的註釋聲明非null作為參數和返回值。
  • @NonNullFields:包級別的註釋聲明字段默認非空

Spring Framework 本身利用了上面這幾個註釋,但它們也可以運用在任何基於Spring的Java 項目中,以聲明空安全api 和 空安全字段。尚未支持泛型和數組元素的可空性,但應也即將發布在後來的版本。Spring Null-Safety出現在Spring5中,讓我們更方便的編寫空安全的代碼,這叫做null-safety,null-safety不是讓我們逃脫不安全的代碼,而是在編譯時產生警告。 此類警告可以在運行時防止災難性空指針異常(NPE)。

@NonNull

@NonNull註釋是null-safety的所有註釋中最重要的一個,我們可以使用此註釋在期望對象引用的任何地方聲明非空約束:字段、方法參數或者方法返回值。

先來看一個例子

public class Student {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

上述代碼對name的校驗是有效的,但是存在一個缺陷,如果name被設置為null的話,那麼當我們使用name的時候,就會以NullPointerException來結尾。

使用@NonNull

Spring 的null-safety特性能夠允許idea或者eclipse報告這個潛在的威脅,例如,如果我們用IDEA對屬性加上@NonNull會出現如下的效果。

奇怪,並沒有什麼變化啊,沒看見有潛在的安全提示啊,那是因為你沒有在idea進行設置

設置安全檢查

如果你也沒有提示的話,可以通過如下的方式設置安全檢查

如果還不好使的話,那就在右側 configuration annotations 添加一下 @NonNull和 @Nullable 所在的jar包,如下:

添加上,打上 ✅ 即可看到如下效果。

現在fullName 已經被@NonNull 註釋添加編譯器檢查null值的功能了!

如果你不相信的話,可以把@NonNull 註釋去掉,你的鼠標再放在fullName 上,已經沒有這句提示了。

@NonNullFields

@NonNull 註解能夠幫助你確保null-safety。然而,如果此註釋直接裝飾所有的字段的話,就會污染整個代碼庫。

Spring提供了另外一個不允許為null的註解 — @NonNullFields。這個註解適合用在包級別上,通知我們的開發工具註釋包中所有的字段,默認的,不允許為null

新建一個Parent類,並在該類所屬包下創建一個名為package-info.java的類,創建的不是Java類,而是創建的file,名為package-info.java,如下

package-info.java

@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullFields;

新建一個Parent.java

public class Parent {

    private String son;
    private String age;
    private String name;

    public void setSon(String son) {
        if(son != null && son.isEmpty()){
            son = null;
        }
        this.son = son;
    }

    public void setAge(String age) {
        if(age != null && age.isEmpty()){
            age = null;
        }
        this.age = age;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

package-info.java 中的@NonNullFields能夠對Parent類中所有的屬性起作用,把鼠標放在任意一個屬性上,會出現編譯期檢查的提示

@Nullable

@NonNullFields註釋通常比@NonNull更好,因為它有助於減少樣板。 但是,有時我們想要從包級別指定的非null約束中免除某些字段,這時候就會使用到@Nullable註解

改造一下Person.java,Person.java 與pack-info.java 處於同一包下

public class Person {

    @NonNull
    private String fullName;

    @Nullable
    private String nickName;

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        if(nickName != null && nickName.isEmpty()){
            nickName = null;
        }
        this.nickName = nickName;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        if(fullName != null && fullName.isEmpty()){
            fullName = null;
        }
        this.fullName = fullName;
    }
}

在這種情況下,我們使用@Nullable註釋來覆蓋字段上@NonNullFields的語義。

@NonNullApi

@NonNullFields註釋僅適用於其名稱所示的字段。 如果我們想對方法的參數和返回值產生相同的影響,我們需要@NonNullApi。

添加 @NonNullApi和 @NonNullFields 在 configure annotations 中,並選用NonNullApi

與@NonNullFields一樣,我們需要在package-info.java 中定義@NonNullApi

package-info.java

@NonNullApi
@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

加上如下註釋后的效果如下: 可以在返回值的時候接受到編譯期的提示。

後記

看完文章,你至少應該了解

  • 四個註解 @NonNull, @Nullable, @NonNullFields, @NonNullApi 四個註解各自的作用範圍
  • 如何設置編譯期的Null-safety檢查

歡迎關注

【精選推薦文章】

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

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

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

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

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

為什麼阿里巴巴要求謹慎使用ArrayList中的subList方法

GitHub 3.7k Star 的Java工程師成神之路 ,不來了解一下嗎?

GitHub 3.7k Star 的Java工程師成神之路 ,真的不來了解一下嗎?

GitHub 3.7k Star 的Java工程師成神之路 ,真的確定不來了解一下嗎?

集合是Java開發日常開發中經常會使用到的。在之前的一些文章中,我們介紹過一些關於使用集合類應該注意的事項,如《為什麼阿里巴巴禁止在 foreach 循環里進行元素的 remove/add 操作》、《為什麼阿里巴巴建議集合初始化時,指定集合容量大小》等。

關於集合類,《阿里巴巴Java開發手冊》中其實還有另外一個規定:

本文就來分析一下為什麼會有如此建議?其背後的原理是什麼?

subList

subList是List接口中定義的一個方法,該方法主要用於返回一個集合中的一段、可以理解為截取一個集合中的部分元素,他的返回值也是一個List。

如以下代碼:

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    List subList = names.subList(0, 1);
    System.out.println(subList);
}

以上代碼輸出結果為:

[Hollis]

如果我們改動下代碼,將subList的返回值強轉成ArrayList試一下:

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    ArrayList subList = names.subList(0, 1);
    System.out.println(subList);
}

以上代碼將拋出異常:

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

不只是強轉成ArrayList會報錯,強轉成LinkedList、Vector等List的實現類同樣也都會報錯。

那麼,為什麼會發生這樣的報錯呢?我們接下來深入分析一下。

底層原理

首先,我們看下subList方法給我們返回的List到底是個什麼東西,這一點在JDK源碼中註釋是這樣說的:

Returns a view of the portion of this list between the specifiedfromIndex, inclusive, and toIndex, exclusive.

也就是說subList 返回是一個視圖,那麼什麼叫做視圖呢?

我們看下subList的源碼:

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

這個方法返回了一個SubList,這個類是ArrayList中的一個內部類。

SubList這個類中單獨定義了set、get、size、add、remove等方法。

當我們調用subList方法的時候,會通過調用SubList的構造函數創建一個SubList,那麼看下這個構造函數做了哪些事情:

SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;
}

可以看到,這個構造函數中把原來的List以及該List中的部分屬性直接賦值給自己的一些屬性了。

也就是說,SubList並沒有重新創建一個List,而是直接引用了原有的List(返回了父類的視圖),只是指定了一下他要使用的元素的範圍而已(從fromIndex(包含),到toIndex(不包含))。

所以,為什麼不能講subList方法得到的集合直接轉換成ArrayList呢?因為SubList只是ArrayList的內部類,他們之間並沒有集成關係,故無法直接進行強制類型轉換。

視圖有什麼問題

前面通過查看源碼,我們知道,subList()方法並沒有重新創建一個ArrayList,而是返回了一個ArrayList的內部類——SubList。

這個SubList是ArrayList的一個視圖。

那麼,這個視圖又會帶來什麼問題呢?我們需要簡單寫幾段代碼看一下。

1、非結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    subList.set(1, "666");

    System.out.println("subList.set(3,666) 得到List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}

得到結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 得到List :
subList : [L, L, I]
subList.set(3,666) 得到List :
subList : [L, 666, I]
sourceList : [H, O, L, 666, I, S]

當我們嘗試通過set方法,改變subList中某個元素的值得時候,我們發現,原來的那個List中對應元素的值也發生了改變。

同理,如果我們使用同樣的方法,對sourceList中的某個元素進行修改,那麼subList中對應的值也會發生改變。讀者可以自行嘗試一下。

1、結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    subList.add("666");

    System.out.println("subList.add(666) 得到List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}

得到結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 得到List :
subList : [L, L, I]
subList.add(666) 得到List :
subList : [L, L, I, 666]
sourceList : [H, O, L, L, I, 666, S]

我們嘗試對subList的結構進行改變,即向其追加元素,那麼得到的結果是sourceList的結構也同樣發生了改變。

1、結構性改變原List

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    sourceList.add("666");

    System.out.println("sourceList.add(666) 得到List :");
    System.out.println("sourceList : " + sourceList);
    System.out.println("subList : " + subList);

}

得到結果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
    at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
    at java.util.AbstractList.listIterator(AbstractList.java:299)
    at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
    at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    at java.lang.String.valueOf(String.java:2994)
    at java.lang.StringBuilder.append(StringBuilder.java:131)
    at com.hollis.SubListTest.main(SubListTest.java:28)

我們嘗試對sourceList的結構進行改變,即向其追加元素,結果發現拋出了ConcurrentModificationException。關於這個異常,我們在《一不小心就踩坑的fail-fast是個什麼鬼?》中分析過,這裏原理相同,就不再贅述了。

小結

我們簡單總結一下,List的subList方法並沒有創建一個新的List,而是使用了原List的視圖,這個視圖使用內部類SubList表示。

所以,我們不能把subList方法返回的List強制轉換成ArrayList等類,因為他們之間沒有繼承關係。

另外,視圖和原List的修改還需要注意幾點,尤其是他們之間的相互影響:

1、對父(sourceList)子(subList)List做的非結構性修改(non-structural changes),都會影響到彼此。

2、對子List做結構性修改,操作同樣會反映到父List上。

3、對父List做結構性修改,會拋出異常ConcurrentModificationException。

所以,阿里巴巴Java開發手冊中有另外一條規定:

如何創建新的List

如果需要對subList作出修改,又不想動原list。那麼可以創建subList的一個拷貝:

subList = Lists.newArrayList(subList);
list.stream().skip(strart).limit(end).collect(Collectors.toList());

PS:最近,《阿里巴巴Java開發手冊》已經正式更名為《Java開發手冊》,併發布了新版本,增加了21條新規約,修改描述112處。

關注公眾號後台回復:手冊,即可獲取最新版Java開發手冊。

參考資料: https://www.jianshu.com/p/5854851240df https://www.cnblogs.com/ljdblog/p/6251387.html

【精選推薦文章】

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

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

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

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

【機器學習】算法原理詳細推導與實現(二):邏輯回歸

【機器學習】算法原理詳細推導與實現(二):邏輯回歸

在上一篇算法中,線性回歸實際上是 連續型 的結果,即 \(y\in R\) ,而邏輯回歸的 \(y\) 是離散型,只能取兩個值 \(y\in \{0,1\}\),這可以用來處理一些分類的問題。

logistic函數

我們可能會遇到一些分類問題,例如想要劃分 鳶尾花 的種類,嘗試基於一些特徵來判斷鳶尾花的品種,或者判斷上一篇文章中的房子,在6個月之後能否被賣掉,答案是 或者 ,或者一封郵件是否是垃圾郵件。所以這裡是 \(x\) ,這裡是 \(y\) 在一個分類問題中,\(y\) 只能取兩個值0和1,這就是一個二元分類的問題,如下所示:

可以使用線性回歸對以上數值進行劃分,可以擬合出如下那麼一條線,用 \(y=0.5\) 作為臨界點,如果 \(x\) 在這個臨界點的右側,那麼 \(y\) 的值就是1,如果在臨界點的左側,那麼 \(y\) 的值就是0,所以確實會有一些人會這麼做,用線性回歸解決分類問題:

線性回歸解決分類問題,有時候它的效果很好,但是通常用線性回歸解決像這樣的分類問題會是一個很糟糕的主意,加入存在一個額外的訓練樣本 \(x=12\),如果現在對這個訓練集合做線性擬合,那麼可能擬合出來那麼一條直線:

這時候\(y\)的臨界點估計已經不太合適了,可以知道線性回歸對於分類問題來說,不是一個很好的方法。

假設 \(h_\theta(x) \in [0,1]\),當如果已知 \(y\in \{0,1\}\),那麼至少應該讓假設 \(h_\theta(x)\) 預測出來的值不會比1大太多,也不會比0小太多,所以一般不會選擇線性函數作為假設,而是會選擇一些稍微不同的函數圖像:

\[ g(z)=\frac{1}{1+e^{-z}} \]

\[ h_\theta(x)=g(\theta^Tx)=\frac{1}{1+e^{-\theta^Tx}} \]

\(g(z)\) 被稱為 sigmoid函數 ,也通常被稱為 logistic函數,它的函數圖像是:

\(z\) 變得非常小的時候,\(g(x)\) 會趨向於0,當\(z\)變得非常大的時候,\(g(x)\) 會趨向於1,它和縱軸相較於0.5。

邏輯回歸

那麼我們的假設\(h_\theta(x)\) 要嘗試估計 \(y\in \{0,1\}\) 的概率,即:

\[ P(y=1|x;\theta)=h_\theta(x) \]

\[ P(y=0|x;\theta)=1-h_\theta(x) \]

以上可以把兩個公式合併簡寫為(如果\(y=1\)那麼公式為\(h_\theta(x)\);如果\(y=0\)那麼公式為\(1-h_\theta(x)\)):

\[ P(y|x;\theta)=(h_\theta(x))^y(1-h_\theta(x))^{1-y} \]

如果對《概率論和數理統計》學得好的人不難看出,以上函數其實就是 伯努利分佈 的函數。

對於每一個假設值\(h_\theta(x)\),為了使每一次假設值更準確,即當 \(y=1\) 時估計函數 \(P(y=1|x;\theta)=h_\theta(x)\) 趨向於1,當\(y=0\) 時估計函數 \(P(y=0|x;\theta)=1-h_\theta(x)\) 趨向於0。則對於每一個\((x_i,y_i)\),參數 \(\theta\) 的似然估計 \(L(\theta)\)為:

\[ \begin{split} L(\theta)&=P(\vec{y}|X;\theta) \\ &=\prod_{i=1}^mP(y^{(i)}|x^{(i)};\theta) \\ &=\prod_{i=1}^m(h_\theta(x^{(i)}))^{y^{(i)}}(1-h_\theta(x^{(i)}))^{1-{y^{(i)}}} \end{split} \]

如果每一個\((x_i,y_i)\)都準確,即 \(P(y|x;\theta)\) 趨向於1,則應該使似然估計 \(L(\theta)\) 最大化,也就是轉化成熟悉的問題:求解 \(L(\theta)\) 的極大似然估計

為了調整參數 \(\theta\) 使似然估計 \(L(\theta)\) 最大化,推導如下(取 \(log\) 是為了去掉疊乘方便計算):

\[ \begin{split} l(\theta)&=logL(\theta) \\ &=\sum_{i=1}^m{y^{(i)}logh(x^{(i)})+(1-y^{(i)})log(1-h(x^{(i)}))} \end{split} \]

為了使這個函數最大,同樣可以使用前面學習過的梯度下降算法使對數似然估計最大化。之前學習的是要使誤差和 最小化,所以梯度下降的公式為:

\[ \theta:=\theta-\alpha\frac{\partial J(\theta)}{\partial\theta}=>\theta:=\theta-\alpha\nabla_\theta J(\theta) \]

而本次為了求解似然估計最大化,使用的是梯度上升:

\[ \theta:=\theta+\alpha\nabla_\theta l(\theta)=>\theta:=\theta+\alpha\frac{\partial l(\theta)}{\partial\theta} \]

對數似然性是和 \(\theta\) 有關,同樣的為了計算 梯度上升 最快的方向,要對上述公式求偏導得到極值,即是上升最快的方向:

\[ \begin{split} \frac{\partial l(\theta)}{\partial\theta_j}&=(y\frac{1}{g(\theta^Tx)}-(1-y)\frac{1}{1-g(\theta^Tx)})\frac{\partial}{\partial\theta_j}g(\theta^Tx) \\ &=(y\frac{1}{g(\theta^Tx)}-(1-y)\frac{1}{1-g(\theta^Tx)})g(\theta^Tx)(1-g(\theta^Tx))\frac{\partial}{\partial\theta_j}\theta^Tx \\ &=(y(1-g(\theta^Tx))-(1-y)g(\theta^Tx))x_j \\ &=(y-g(\theta^Tx))x_j \\ &=(y-h_{\theta}(x))x_j \end{split} \]

則對於 m 個樣本,則有:

\[ \frac{\partial l(\theta)}{\partial\theta_j}=\sum_{i=1}^m{(y-h_{\theta}(x))x_j} \]

\[ \theta_j:=\theta_j+\sum_{i=1}^m{(y^{(i)}-h_{\theta}(x^{(i)}))x^{(i)}_j} \]

所以總結來說:

邏輯回歸假設數據服從伯努利分佈,通過極大化似然函數的方法,運用梯度下降來求解參數,來達到將數據二分類的目的。

鳶尾花分類

為了劃分 鳶尾花 的種類,嘗試基於一些特徵來判斷鳶尾花的品種,選取100條鳶尾花數據集如下所示:

花萼長度(單位cm) 花萼寬度(單位cm) 種類
5.1 3.5 0
4.9 3.0 0
4.7 3.2 0
7.0 3.2 1
6.4 3.2 1

其中:

種類 含義
0 山鳶尾(setosa)
1 變色鳶尾(versicolor)
2 維吉尼亞鳶尾(virginica)

數據集的圖像分佈為:

計算損失函數:

# 損失函數
def computeCost(theta, X, y):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)
    first = np.multiply(-y, np.log(sigmoid(X * theta.T)))
    second = np.multiply((1 - y), np.log(1 - sigmoid(X * theta.T)))
    return np.sum(first - second) / (len(X))

梯度下降函數為:

# 梯度下降
def gradient(theta, X, y):
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)

    parameters = int(theta.ravel().shape[1])
    grad = np.zeros(parameters)

    error = sigmoid(X * theta.T) - y

    for i in range(parameters):
        term = np.multiply(error, X[:, i])
        grad[i] = np.sum(term) / len(X)

    return grad

最終預測準確率為:

accuracy = 99%

結果分類的圖像為:

數據和代碼下載請關注公眾號【 TTyb 】,後台回復【 機器學習 】即可獲取:

【精選推薦文章】

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

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

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

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

簡潔方便的集合處理——Java 8 stream流

背景

java 8已經發行好幾年了,前段時間java 12也已經問世,但平時的工作中,很多項目的環境還停留在java1.7中。而且java8的很多新特性都是革命性的,比如各種集合的優化、lambda表達式等,所以我們還是要去了解java8的魅力。

今天我們來學習java8的Stream,並不需要理論基礎,直接可以上手去用。

我接觸stream的原因,是我要搞一個用戶收入消費的數據分析。起初的統計篩選分組都是打算用sql語言直接從mysql里得到結果來展現的。但在操作中我們發現這樣頻繁地訪問數據庫,性能會受到很大的影響,分析速度會很慢。所以我們希望能通過訪問一次數據庫就拿到所有數據,然後放到內存中去進行數據分析統計過濾。

接着,我看了stream的API,發現這就是我想要的。

一、Stream理解

在java中我們稱Stream為『』,我們經常會用流去對集合進行一些流水線的操作。stream就像工廠一樣,只需要把集合、命令還有一些參數灌輸到流水線中去,就可以加工成得出想要的結果。這樣的流水線能大大簡潔代碼,減少操作。

二、Stream流程

原集合 —> 流  —> 各種操作(過濾、分組、統計) —> 終端操作

 

Stream流的操作流程一般都是這樣的,先將集合轉為流,然後經過各種操作,比如過濾、篩選、分組、計算。最後的終端操作,就是轉化成我們想要的數據,這個數據的形式一般還是集合,有時也會按照需求輸出count計數。下文會一一舉例。

三、API功能舉例

首先,定義一個用戶對象,包含姓名、年齡、性別和籍貫四個成員變量:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Log4j
@Builder
public class User {
    //姓名
    private String name;
    //年齡
    private Integer age;
    //性別
    private Integer sex;
    //所在省市
    private String address;
}

 

這裏用lombok簡化了實體類的代碼。

然後創建需要的集合數據,也就是源數據:

//1.構建我們的list
List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤",20,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

3.1 過濾

1)創建流 stream() / parallelStream()

  • stream() : 串行流
  • parallelStream(): 并行流

2)filter 過濾(T-> boolean)

比如要過濾年齡在40歲以上的用戶,就可以這樣寫:

List<User> filterList = list.stream().filter(user -> user.getAge() >= 40)
        .collect(toList());

 

filter裏面,->箭頭後面跟着的是一個boolean值,可以寫任何的過濾條件,就相當於sql中where後面的東西,換句話說,能用sql實現的功能這裏都可以實現

打印結果:

3)distinct 去重

和sql中的distinct關鍵字很相似。為了看到效果,此處在原集合中加入一個重複的人,就選擇鋼鐵俠吧,復聯4鋼鐵俠不幸遇害,大家還是比較傷心的。

List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤”,18,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

//distinct 去重
List<User> distinctList = filterList.stream().distinct()
        .collect(toList());

 

打印結果:

4)sorted排序

如果流中的元素的類實現了 Comparable 接口,即有自己的排序規則,那麼可以直接調用 sorted() 方法對元素進行排序,如: 

Comparator.comparingInt

 

反之, 需要調用 sorted((T, T) -> int) 實現 Comparator 接口。

//sorted()
List<User> sortedList = distinctList.stream().sorted(Comparator.comparingInt(User::getAge))
        .collect(toList());

 

打印結果:

結果按照年齡從小到大進行排序。

5)limit() 返回前n個元素

如果想知道這裏面年齡最小的是誰,可作如下操作:

//limit 返回前n個元素
List<User> limitList = sortedList.stream().limit(1)
        .collect(toList());

 

6)skip()

與limit恰恰相反,skip的意思是跳過,也就是去除前n個元素。

打印結果:

果然,前兩個人都被去除了,只剩下最老的葫蘆娃爺爺。

3.2 映射

1)map(T->R)

map是將T類型的數據轉為R類型的數據,比如我們想要設置一個新的list,存儲用戶所有的城市信息。

//map(T->R)
List<String> cityList = list.stream().map(User::getAddress).distinct().collect(toList());

 

打印結果:

2)flatMap(T -> Stream)

將流中的每一個元素 T 映射為一個流,再把每一個流連接成為一個流。

//flatMap(T -> Stream<R>)
List<String> flatList = new ArrayList<>();
flatList.add("唱,跳");
flatList.add("rape,籃球,music");
flatList = flatList.stream().map(s -> s.split(",")).flatMap(Arrays::stream).collect(toList());

 

打印結果:

這裏原集合中的數據由逗號分割,使用split進行拆分后,得到的是Stream<string[]>,字符串數組組成的流,要使用flatMap的

Arrays::stream

將Stream<string[]>轉為Stream,然後把流相連接,組成了完整的唱、跳、rap、籃球和music。

3.3 查找

1)allMatch(T->boolean)

檢測是否全部滿足參數行為,假如這些用戶是網吧上網的用戶名單,那就需要檢查是不是每個人都年滿18周歲了。

boolean isAdult = list.stream().allMatch(user -> user.getAge() >= 18);

 

打印結果:

true

 

2)anyMatch(T->boolean)

檢測是否有任意元素滿足給定的條件,比如,想知道同學名單里是否有女生。

//anyMatch(T -> boolean) 是否有任意一個元素滿足給定的條件
boolean isGirl = list.stream().anyMatch(user -> user.getSex() == 1);

 

打印結果:

true

 

說明集合中有女生存在。

3)noneMatch(T -> boolean)

流中是否有元素匹配給定的 T -> boolean 條件。

比如檢測有沒有來自巴黎的用戶。

boolean isLSJ = list.stream().noneMatch(user -> user.getAddress().contains("巴黎"));

 

打印結果:

true

 

打印true說明沒有巴黎的用戶。

4)findFirst( ):找到第一個元素

Optional<User> fristUser  = list.stream().findFirst();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

5)findAny():找到任意一個元素

Optional<User> anyUser  = list.stream().findAny();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

這裏我們發現findAny返回的也總是第一個元素,那麼為什麼還要進行區分呢?因為在并行流 parallelStream() 中找到的確實是任意一個元素。

Optional<User> anyParallelUser  = list.parallelStream().findAny();

 

打印結果 :

Optional[User(name=李世民, age=60, sex=0, address=山西省太原市)]

 

3.4 歸納計算

1)求用戶的總人數

long count = list.stream().collect(Collectors.counting());

 

我們可以簡寫為:

long count = list.stream().count();

 

運行結果:

 8

 

2)得到某一屬性的最大最小值

// 求最大年齡
Optional<User> max = list.stream().collect(Collectors.maxBy(
Comparator.comparing(User::getAge)));

// 求最小年齡
Optional<User> min = list.stream().collect(Collectors.minBy(
Comparator.comparing(User::getAge)));

 

運行結果:

3)求年齡總和是多少

// 求年齡總和
int totalAge = list.stream().collect(Collectors.summingInt(User::getAge));

 

運行結果:

 313

 

我們經常會用BigDecimal來記錄金錢,假設想得到BigDecimal的總和:

// 獲得列表對象金額, 使用reduce聚合函數,實現累加器
BigDecimal sum = myList.stream() .map(User::getMoney)
.reduce(BigDecimal.ZERO,BigDecimal::add);

 

4)求年齡平均值

//求年齡平均值
double avgAge = list.stream().collect(
Collectors.averagingInt(User::getAge));

 

運行結果:

 39.125

 

5)一次性得到元素的個數、總和、最大值、最小值

IntSummaryStatistics statistics = list.stream().collect(
Collectors.summarizingInt(User::getAge));

 

運行結果:

6)字符串拼接

要將用戶的姓名連成一個字符串並用逗號分割。

String names = list.stream().map(User::getName)
.collect(Collectors.joining(", "));

 

運行結果:

 鋼鐵俠, 鋼鐵俠, 蜘蛛俠, 趙麗穎, 詹姆斯, 李世民, 蔡徐坤, 葫蘆娃的爺爺

 

3.5 分組

在數據庫操作中,我們經常通過GROUP BY關鍵字對查詢到的數據進行分組,java8的流式處理也提供了分組的功能。使用Collectors.groupingBy來進行分組。

1)可以根據用戶所在城市進行分組

Map<String, List<User>> cityMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress));

 

結果是一個map,key為不重複的城市名,value為屬於該城市的用戶列表。已經實現了分組。

2)二級分組,先根據城市分組再根據性別分組

Map<String, Map<Integer, List<User>>> group = list.stream().collect(
        Collectors.groupingBy(User::getAddress, // 一級分組,按所在地區
                Collectors.groupingBy(User::getSex))); // 二級分組,按性別

 

運行結果:

3)如果僅僅想統計各城市的用戶個數是多少,並不需要對應的list

按城市分組並統計人數:

Map<String, Long> cityCountMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

4)當然,也可以先進行過濾再分組並統計人數

Map<String,Long> map = list.stream().filter(user -> user.getAge() <= 30)
        .collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

5)partitioningBy 分區

分區與分組的區別在於,分區是按照 true 和 false 來分的,因此partitioningBy 接受的參數的 lambda 也是 T -> boolean

//根據年齡是否小於等於30來分區
Map<Boolean, List<User>> part = list.stream()
        .collect(partitioningBy(user -> user.getAge() <= 30));

 

運行結果:

總結

到目前為止,stream的功能我們已經用了很多了,感覺有點眼花繚亂卻無所不能,stream能做的事情遠遠不止這些。

我們可以多學習使用stream,把原來複雜的sql查詢,一遍又一遍地for循環的複雜代碼重構,讓代碼更簡潔易懂,可讀性強。

拓展閱讀:Redis專題(1):構建知識圖譜

Redis專題(2):Redis數據結構底層探秘

作者:楊亨

來源:宜信技術學院

【精選推薦文章】

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

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

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

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

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

Amzaon EC2虛擬化技術演進:從 Xen 到 Nitro

  今年2月,由光環新網運營的 AWS 中國(北京)區域和由西雲數據運營的 AWS 中國 (寧夏)區域發布新的實例類型,新的實例類型包括 C5、C5d、R5、R5d。除了這四種之外,在AWS國外部分區域還上線了最新的C5n。       這些新實例類型個個都具有鮮明的特徵,我簡單整理歸納如下:

  • C5實例:性價比顯著提升(與 C4 實例相比,C5 實例提供了更高的內存與 vCPU 比率,並且性價比提高了 25%,某些應用程序提高了 50% 以上),更大的實例大小(C5 實例新的更大的實例 c5.18xlarge提供了 72 個 vCPU 和 144 GiB 內存並提供了 25 Gbps 的網絡帶寬)。
  • C5d實例:基於本地 NVMe 的 SSD 磁盤將被物理連接到主機服務器,提供與C5實例的生命周期相耦合的塊級存儲。c5d.18xlarge 規格的實例支持2塊900GB的NVMe SSD作為本地存儲。
  • C5n實例:這是C5 系列的最新成員,其c5n.18xlarge規格可提供高達 100Gbps 的網絡吞吐量。
  • R5實例:其最大實例規格支持96 vCPU、768 GiB內存和25 Gbps 網絡帶寬。
  • R5d實例:R5d 實例與 R5 實例規格相同,它還包括高達 3.6 TB 的本地 NVMe 存儲。

這些實例類型之所以如此實力超群,我認為主要歸功於兩點:

  • 處理器升級

C5 實例配備 Intel Xeon Platinum 8000 系列 (Skylake-SP) 處理器,它發佈於2017/Q3,具有高達 3.4GHz 的穩定全核 Turbo CPU 時鐘速度,並使用 Intel Turbo Boost Technology 來允許單個核心睿頻高達3.5GHz。C5 實例為新的 Intel 高級矢量擴展 512 (AVX-512) 指令集提供了支持,與上一代 C4 實例相比,矢量和浮點計算性能提高最高可達2倍。
而發佈於2015年的C4 實例類型,配備Intel Xeon E5-2666 v3 (Haswell) 處理器。其時鐘頻率為2.9 GHz,配合Intel® Turbo Boost后最高可達3.5 GHz。

  • 採用了AWS Nitro 虛擬化平台

AWS Nitro 將是這篇文章的主角。本文會從它的發展歷程、架構、所創造的價值等方面進行分析和介紹,試圖總結出AWS上虛擬化基礎平台發展的脈絡。

AWS EC2虛擬化發展歷程

下錶總結了AWS曾經採用的虛擬化技術,以及這些技術之間的性能對比:

 

  • #1是全模擬技術。這種虛擬化方式能支持未修改的客戶機操作系統,但速度會嚴重下降。典型產品是VMware 在1986年發布的虛擬化產品。AWS 並沒有採用這種虛擬化技術,放在表格中只是為了做對比用。
  • #2 是基於Xen的半虛擬化技術(Paravirtualization,PV)。PV 要求修改客戶機內核和驅動。EC2第一個採用半虛擬化的實例類型是 m1.small。
  • #3 到 #6 是基於Xen和CPU硬件的全虛擬化技術(Hardware-assisted virtualization,HVM)。採用Xen HVM 技術的虛擬機運行在具有CPU和內存(VT-x)硬件虛擬化能力的處理器上,並使用半虛擬化驅動程序用於網絡和存儲設備。HVM 3.0 中尚未實現中斷和定時器半虛擬化,但在4.0中已有改善。
  • #7 和 #8 則是AWS Nitro技術,這是AWS 研發的一種新虛擬化平台。後面會有詳細介紹。

過去幾年中,Xen是AWS上虛擬化技術的主體,業已成為業界標準之一,已經非常成熟。那麼,為什麼AWS要從Xen 向 Nitro 發展呢?這得從Xen 的架構說起。

從上圖可以看出,Xen 實現了虛擬機的CPU 和內存虛擬化,但是虛擬機的I/O 訪問,包括網絡和存儲等,都是通過虛擬機中的前端模塊和 dom0 中的後端模塊通信,然後由dom0 中的後端模塊通過設備驅動實現的。這I/O路徑太長,這降低了I/O性能,而且dom0還會和業務虛擬機搶佔宿主機資源,很難實現管理虛機和業務虛機之間的平衡,以及避免抖動。

2013年,AWS 採用 Xen PV虛擬化技術的cr1.8xlarge 實例的架構如下圖所示:

這是嚴格意義上未採用Nitro技術的最後一個EC2型號。簡要說明:

  1. 圖中的硬件(Hardware),是運行虛擬機的物理服務器,採用了當時很強大的標準的10Gbps網卡,以及管理一些本地磁盤的存儲HBA卡。Hardware上既運行用戶的業務虛擬機,還運行Xen的dom0虛擬機。
  2. VMM採用Xen項目的PV模式。
  3. 圖中 Amzon Linux 代表Xen dom0,它負責訪問硬件,向虛擬機提供I/O 能力。

圖中 cr1.8xlarge 代表一個這種規格的虛擬機,它的本地存儲、EBS卷和VPC網絡訪問都是通過Xen管理的dom0 虛擬機實現的。

Nitro起源和發展

針對傳統虛擬化架構存在的問題,從2012年開始,AWS EC2虛擬化團隊就開始思考以下問題:

  1. 能做出比純軟件架構更好的hypervisor嗎?
  2. 設備模型本身很複雜,而且它會和業務虛擬機競爭CPU和系統資源,同時技術上它很難避免抖動發生
  3. hypervisor太重了,能將hypervisor 和它周邊的組件解耦嗎?

  從成立之日起,AWS就善於聽取客戶的呼聲和建議,並不斷進行迭代式改進,而不是大刀闊斧地從頭設計一個新架構。根據該原則,AWS團隊首先從最難的網絡部分着手,其位置就是上圖中的金黃色虛線框所示位置。從2013年開始,一些EC2實例類型開始支持網絡接口的硬件虛擬化:單根I/O虛擬化(SR-IOV),而第一個是2013年1月發布的C3,它首次採用了AWS增強型網絡(enhanced networking)。這最初是通過ixgbe驅動程序實現的,速度高達10 Gbps。   c3.8xlarge的架構如下圖所示:

c3.8xlarge的架構與cr1.8xlarge相比,在宿主機上增加了一塊新網卡,這塊網卡和原有的標準網絡通過一個迴環線(loopback cable)連接起來。虛機VPC網絡功能不再通過Xen 的dom0 實現,而是直接訪問宿主機上的這塊硬件網卡。C3 是AWS EC2 歷史上增長最快的幾個實例類型之一,它尤其以控制性能抖動和持續的網絡性能著稱。這可以看做Nitro思想的發源,那就是將軟件功能卸載到專有硬件上。

  下一個改進方向是EBS存儲訪問性能提升。   2015年,AWS推出了C4實例類型,它針對EBS卷使用了硬件虛擬化技術。c4.8xlarge的架構如下圖所示。仔細對比能發現,這個新架構與C3中的網絡架構改進有些不同。在虛擬機中,還保留了“前端-後端”這種Xen傳統架構,這是當時為了兼容性和穩妥新考慮,因為NVMe在當時來說還是一種非常新的技術。在宿主機上,採用了新收購的Annapurna Labs公司開發一種卡(下圖中黃色虛線框內),它能將遠端存儲以NMVe形式呈現給虛擬機。

這個改進的結果是,宿主機上的CPU被Xen佔用得少了,能更多地被虛機使用了。

  2016年5月發布的X1 是第一個支持ENA的實例類型。ENA是增強型網絡的最新實現,速度高達25 Gbps。ENA,全稱是Elastic Network Adapter,它正是Nitro項目的一部分,它是由Annapurna Labs公司開發的。

現在的ENA,能用於虛擬機和物理機,它以開源項目形式發布在github上。ENA 是AWS網絡虛擬化一關鍵技術,它使得虛擬機能夠繞過內核和用戶空間網絡處理程序,直接操作網卡硬件,這顯著提升了網絡效率。

從用戶使用角度,也許只是用了一個新網卡驅動。但是其底層採用了Annapurna Labs公司開發的定製網絡ASIC硬件卡。這是Nitro第一款真正的專用硬件卡。它不僅卸載了VPC網絡功能,還卸載了EBS 存儲網絡功能。因此這是一種完全的網絡負載卸載硬件。

 

下一步的優化方向在實例存儲上。2017年,AWS發布了存儲優化實例類型i3,它使用了SR-IOV和NVMe存儲驅動。這是AWS首次採用Annapurna Labs研發的Nitro存儲卡40202所管理的SSD磁盤,這些磁盤被直接映射給虛擬機,虛擬機通過NVME驅動來使用宿主機上的SSD磁盤。這能實現磁盤300萬以上的IOPS性能。Nitro 芯片負責包括磁盤監控、加密、QoS等職責。  

 

  顯然,到這時候為止,仍然剩下的問題只能是Xen 自身,以及它的管理功能部分了。Xen過於笨重,因為作為傳統 Hypervisor,它必須做很多事情 – 它必須保護物理硬件和 BIOS,它必須虛擬化 CPU,虛擬化存儲,虛擬化網絡,並提供豐富的管理功能。其管理性dom0虛擬機會搶佔業務虛機的系統資源。那到底能不能把Xen徹底替換掉呢?答案是肯定的,因為AWS在技術上從來沒讓人失望過。   2017年11月,AWS發布了C5 實例類型。它使用基於KVM的Nitro hypervisor 替換了Xen,hypervisor 軟件大大被簡化,Xen 所用的 dom0 也不需要了。其架構示意圖如下:

 

AWS Nitro 則重新構建了EC2虛擬化基礎架構。Nitro 系統將存儲、網絡和安全功能卸載(offload)到專用的硬件(Nitro卡)上,帶來的好處是虛擬化實例幾乎可以為客戶機操作系統提供主機的所有 CPU 和內存,同時Hypervisor 的功能也因此大大減弱。   Nitro 還被用到2017年發布的AWS 首個物理機實例類型 i3.metal中。下圖是i3.metal架構示意圖:

在i3.metal 中,Nitro 發揮了基礎性作用。它的安全芯片通過提供硬件保護和固件驗證功能為I3實例提供安全保障;它的各種卡,使得I3實例具備基於非易失性存儲器標準 (NVMe) SSD 的實例存儲,通過ENA支持高達 25Gbps 的聚合網絡帶寬。 

Nitro 架構

AWS Nitro 系統是模塊化組件的集合,可以使用廣泛的計算、存儲、內存和網絡選項來設計 EC2 實例,為新一代EC2實例提供動力。它包括三大部分:

 

Nitro 卡

 

這些Nitro 卡是硬件,插入到宿主機的PCIe卡槽中,採用SR-IOV 直通(passthrough)技術將這些卡呈現給實例。包括:

  • VPC Data Plane(用於VPC訪問的Nitro卡):本質上是一塊通過PCIe附加到宿主機上的一塊定製網卡,支持網絡封包和解包、安全組、限速器和路由等功能。實例通過ENA驅動和它通信。同時,該卡還帶有一些網絡加速功能。以限速器為例,每個Nitro支持的實例,不管它在哪個區域哪個數據中心哪個宿主機上,都會有一致的性能,這對分佈式應用非常重要。
  • EBS Data Plane(用於EBS卷訪問的Nitro卡):本質上是一塊通過PCIe附加到宿主機上的一塊定製卡。通過該卡,遠端存儲被以NVMe設備形式展現給實例,實例通過標準NVMe驅動程序訪問該卡。它首次被用在C4中。支持卷加密、存儲加速;支持I3裸機實例。
  • Instance Storage Data Plane(用於實例存儲訪問的Nitro卡):通過該卡,本地磁盤被以NVMe設備形式展現給實例,實例通過標準NVMe驅動程序訪問這些磁盤。支持加密、限速器和本地磁盤監控。

除了卡之外,Nitro 還提供卡控制器(Card Controller)。它提供API端點,負責協調所有Nitro卡、Nitro Hypervisor和Nitro安全芯片。它還利用Nitro安全芯片實現了Hardware Root Of Trust(硬件信任根),支持實例監控、計量和認證。它還為Nitro EBS卡實現了NVMe控制器。

Nitro 安全芯片

Nitro安全芯片整合到宿主機主板中,控制對所有非易失性存儲的訪問,持續監控和保護硬件資源,並在每次系統啟動時獨立驗證固件。

Nitro hypervisor

Nitro hypervisor位於極簡化的定製的Linux 內核中,基於KVM,帶有定製的VMM和小用戶空間應用。它只負責管理內存和CPU分配,將Nitro卡虛擬功能分配給實例,監控和計量硬件等,不再需要提供任何網絡功能。因此它只需執行虛擬機所需指令,快速而且簡單,在大多數工作負載中能提供接近裸機的性能。 Nitro 各組件之間的關係如下圖所示:

 

Nitro 帶來的豐富價值

更高網絡訪問性能

利用Nitro提供的新一代 Elastic Network Adapter (ENA) 和 NVM Express (NVMe) 技術,C5 實例提供了高達 25 Gbps 的網絡帶寬和更低延遲及抖動。2018年發布的更強大變體 C5n 實例,支持網絡帶寬高達 100 Gbps,用戶的仿真、內存緩存、數據湖以及其他通訊密集型應用運行得將比以往更好。   採用Nitro增強網絡功能后的網絡延遲對比:

(Series 1:cc2.8xlarge,2:c3.8xlarge,3:c4.8xlarge,4:c5.18xlarge,5:c5.18xlarge(採用ENAv2))

網絡和存儲帶寬對比:   (1:c3.8xlarge,2:c4.8xlarge,3:c5.18xlarge,4:c5n.18xlarge. Series1:網絡,Series2:存儲)

更高EBS和本地存儲訪問性能

Nitro 使得實例可通過物理方式連接到主機服務器的基於 NVMe 的本地 SSD 塊級存儲,以及將遠端存儲以NVMe設備的形式呈現給實例。 2019年3月,由Nitro支撐的新計算密集型 C5 和 C5d 實例已經在AWS 北京和寧夏區域推出。C5實例支持高達9Gbps 的專用 Amazon EBS 帶寬。而 C5d 最大實例規格則可使用兩塊900G的NVMe SSD。這些實例非常適合需要訪問高速、低延遲的本地存儲的應用程序。

更大實例大小和CPU內存比率

由Nitro支撐的C5實例,其實例的CPU和內存比率,由C4的1:1.875上升到1:2;實例的最大規格,從C4的36vCPU/60Gib內存,上升到72vCPU/144Gib內存。

更低虛擬化花銷

Nitro Hypervisor 是一款輕薄的靜態的虛擬機管理程序,可管理虛擬機的內存和CPU分配,並提供與大多數工作負載無法區分的性能。據Netflix公司Brendan Gregg 觀察,Nitro Hypervisor的性能損耗非常小,通常不到1%,他的結論是 Nitro提供的虛擬化性能接近裸設備。

更低Hypervisor抖動

有了Nitro后,就不再需要為存儲和網絡I/O再預留CPU和內存資源了。這不僅使得可以向EC2實例分配更多資源,為更大的實例規格提供了可能,還為實現一個簡單的輕量的hypervisor提供了可能,而這就為實現更低hypervisor抖動創造了條件。   下圖是一AWS 客戶在三種EC2實例上採用對延遲要求極低的一實時應用做的對比測試。藍色是C5,紅色是i3.metal,黃色是C4。SLA 是用於測試的實時應用所能忍受的最高延遲。

 

從上圖中的測試結果看,C5 相對裸機只有一點極小的附加開銷,而且性能非常平穩,幾乎沒有波動,能完全滿足應用的SLA需求。而C4則有相對較大的波動,只能大概滿足70的SLA。

更多實例類型

AWS發布了基於Nitro的實例存儲實例類型 C5d,M5d 和 R5d,提供低延遲高吞吐的基於NVMe的實例存儲。 AWS在2017 re:Invent上宣布了基於Nitro的AWS EC2 Bare Metal實例 I3.metal。它沒有性能開銷,能夠運行你喜歡的任何東西,比如Xen,KVM,容器,ESXi,FireCracker微虛機等;支持非虛擬化環境,支持容器環境,同時還能繼續使用比如EBS、ELB和VPC等基礎服務;支持比如SAP HANA和其它內存型應用。 AWS還基於Nitro發布了採用AMD EPYC處理器的系列實例R5,M5和T3,最高可降低10%成本。 AWS發布了基於Nitro的具有100Gbps網絡帶寬的實例類型C5n,這是運行HPC和分佈式機器學習負載的理想類型。 AWS發布了基於Nitro的採用AWS Graviton(基於ARM)處理器的實例類型A1,最高可降低45%成本。

更低價格、更高性價比

下錶显示了AWS 北京(BJS)和中衛(ZHY)區域的4代和5代EC2實例的價格比較,你可以看到實實在在的價格下降:  

 

目前,Nitro支撐的C5 實例提供了 EC2 產品系列中最佳的價格/計算性能比。與C4實例相比,其性價比提高了49% 。 與R4實例相比,由Nitro支撐的R5實例為每個vCPU提供額外5%的內存,且每 GiB 價格低50%。R5實例非常適用於高性能數據庫、分佈式內存緩存、內存數據庫和大數據分析等應用程序。

為更多性能優化提供了可能

對於需要深度定製化EC2 的用戶而言,Nitro 還帶了了另外的好處:對於EC2 更深入的監控和優化。在由Nitro支撐的C5實例中,你可以得到數百個PMC 計數器(Performance Monitoring Counters ,性能監控計數器)。作為對比,以前的實例類型中,你只能看到區區7個PMCs。更多的PMC計數器,為性能優化提供了更多可能。

小結

亞馬遜 AWS CTO 沃納·威格爾(Werner Vogels)曾經說過,“在亞馬遜 AWS,我們90%到95%的新項目都是基於客戶給我們的反饋,剩下5%也是從客戶角度出發所做得創新嘗試。”而Nitro 正是這種項目之一,它誕生於2013年,成年於2017年,現在還在不斷成長中。Nitro 正在作為AWS核心虛擬化架構平台,推動着AWS最核心的EC2產品家族不斷往更大(單實例的vCPU和內存更大)、更快(I/O速度更快)、更安全(採用Nitro安全芯片)、更穩定(Hypervisor抖動更低)、更多類型、更高性價比方向演進,支撐越來越多用戶越來越多的業務場景,創造着越來越大的業務價值。     主要參考文檔:

  1. AWS re:Invent 2018: Powering Next-Gen EC2 Instances: Deep Dive into the Nitro System (CMP303-R1)
  2. AWS re:Invent 2017: C5 Instances and the Evolution of Amazon EC2 Virtualization (CMP332)
  3. AWS re:Invent 2018: Deep Dive on Amazon EC2 Instances & Performance Optimization Best Practices (CMP307)
  4. AWS re:Invent 2018:Optimizing Network Performance for Amazon EC2 Instances (CMP308-R1) 

感謝您的閱讀,歡迎關注我的微信公眾號:

 

【精選推薦文章】

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

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

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

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

反射機制 小小談

反射機制(Reflection)

何為反射

反射是在兩種物質分界面上改變傳播方向返回原來物質中的現象
反射是生物體對外界刺激做出應激行為的過程,根據產生的原因分為條件反射非條件反射等,典型的實驗案例包括巴甫洛夫的狗……
反射是一些面向對象程序設計語言提供的針對對象元數據(Metadata)的一種訪問機制

元……數據??什麼高深莫測的武功??

啊,誠然,一旦涉及到“元XXX”事情通常就開始變得無比抽象,以至於我不禁念叨起那句訣

太極生兩儀,兩儀生四象,四象生八卦……

不過元數據這個概念在數據庫里還是比較常見的,比如,某個關係型數據庫里有張表:

水果

編號 名字 數量
1 蘋果 6
2 香蕉 3
3 5
4 橘子 3
5 菠蘿 2

數據,就是存在表裡的一條一條的記錄,(1,蘋果,6),(3,梨,5)都是數據,那麼,元數據就是凌駕於這些數據之上的用於描述數據數據,對於這張表而言,也就是這張表的表頭(關係數據理論里稱之為關係模式):(編號,名稱,數據)

划重點
元數據(Metadata):用於描述數據的數據

好像有些明朗了,但那關面向對象什麼事呢

眾所周知,類(Class)是面向對象的一個重要概念,儘管,針對於數據庫來說,對象模型和關係模型是不同的概念(上文提到的是關係模型的一個例子),但是,對象模型中的對象和關係模型中的關係,其級別是等同的。

關係……又對象……越來越聽不懂了

好吧,我們先把關係放在一邊,我們只把上邊的東西看做一張表。

難道你就沒有把它改寫成如下形式的衝動嗎??

public class Fruit
{
    public int no;
    public string name;
    public int count;
    
    public Fruit(int no, string name, int count)
    {
        // ...
    }
}

好了,上面的類定義的語義就是

有這樣一類東西,我們稱呼這類東西為水果,結構如下……

那麼,這樣一來,我們就可以定義一個no為9,name叫做“西瓜”,count為5的一個對象,這個對象具有具體的數據。

而上面的類定義代碼,包含的就是這個類的元數據

說的再直白點吧

以人為例,數據注重的是這人的臉長啥樣,而元數據注重的是這人有沒有臉(好像不太對……)

好吧差不多了解了,但元數據和反射有什麼關係呢

反射是一些面向對象程序設計語言提供的針對對象元數據(Metadata)的一種訪問機制

本文一開始就說了,罰站20年

不過在此之前先解釋一件事,元數據在哪

任何一個面向對象的程序設計語言,其類類型都具備一個元數據的存儲,至少程序會使用這個元數據能夠動態地構造此類的對象。但不同的語言機制不同,比如C++這種的,因為直接和系統進行愉♂快的互♂動,因此元數據就直接使用系統的內存地址了,這種數據使用是很不直觀的,同時也不使用任何託管機製做後援(巨硬魔改的C++/CLI不在討論範圍內),因此這種貼近底層的語言不支持反射機制,雖然可以通過強行向程序代碼中通過工廠類模式強行注入可讀的元信息(方法參見這位大佬的文章)。

但是,正如前面所說的,如果元數據在託管編譯或解釋的狀態下會保留一份可讀的版本,這是提供給解釋器或者託管平台用的,當然,這種情況下語言一般會提供一個較為完善的元數據訪問機制,這就是反射。這類語言典型的代表就是C#(.NET託管)、Java(JVM虛擬機)、Python(解釋器提供)等。

那……反射是如何運作的呢??

反射嘛。那還不容易,拿個鏡子就可以了呀!
或者用羊角錘偷襲的方式砸膝蓋什麼的也是很容易的呀!
不過這麼說來,拿羊角錘偷襲鏡子豈不是更棒!!

正如之前所說,反射機制是對類的元數據的獲取和操縱,因此,一個重要的前提就是:

這個程序設計語言的運作機制當中,類的元數據必須是可見的,如果可讀的話那更好

只有當類的元數據是可見的,反射機制才有訪問它們的可能,但是元數據的可讀性會決定反射機制訪問它們的難易程度。

這裏補充一句,有人會說,在使用IDE或者代碼編輯器的時候,我們寫object.property這種訪問方式的時候編譯器不就直接告訴我們了么??
關於這一點,這裏暫時只說一個前提:

反射機制的實際動作是聚焦於運行時(Runtime)的。

在程序代碼編譯之前我們恣意地書寫這MyObject.id.hashCode.getFlush().balabala的時候,這是預編譯的過程,預編譯的時候當然這些元數據都是以字面形式給出的(因為你的代碼里寫了這個類的定義),你可以非常愉悅地Ctrl+C Ctrl+V或者享受着IntelliSense帶給你的N倍快樂,這個時候再談反射就沒什麼意義了,因此,反射機制訪問元數據都是在編譯后運行時發生的。

明明都是面向對象,為什麼偏偏C++不支持這個東西呢

以C++為例,這些元數據是否可見?答案是肯定的,那為什麼不支持反射機制呢,因為這些元數據是以指針的方式給出的,指針在已編譯的C++程序中的存在形式就是地址,說的再粗暴點,就是4或8字節的二進制數……
也就是說,在已經編譯完成的C++程序的眼裡,類的元數據已經變成二進制的地址碼了,如果某人在沒有源代碼的情況下想給這個項目寫一個反射機制,那麼他將不得不面對一大堆的:

0xb08dfe231a1c002e
0xb08dfe231bc128f6
0xb08dfe2417a90f5d
......

看到這些,他長舒了一口氣,優雅地點燃了一根香煙,然後毫不猶豫地戳到電腦屏幕上:

鬼知道這是什麼玩意啊!!

如果原項目加個殼、模板元編一下再做個混淆加密的話那更沒法看了,因此如果一定要實現反射機制,一般都是把反射機制直接囊括到項目開發過程當中(就像上面那位大佬的文章中提到的那樣,原項目的作者也是反射機制的構造者)。
這樣的話就會存在一個上上上個世紀汽車行業出現的問題:

這輛車的件無法用到另外一輛車上!
這個反射機制無法用到別的項目上!

當然,這樣說可能有些絕對,但以C++的方式實現一個能夠廣泛用於所有項目的反射機制應該是極端困難的。
上面大佬的文章當中,這個C++的項目要使用反射機制,是藉助工廠模式實現的,關於這些的實現方法,詳見大佬文章(當然我自己也沒完全看懂)

那託管語言又如何呢

C#、Java,這兩種語言都是託管代碼的(C#使用.NET進行託管,Java則交給了JVM虛擬機)。

與C++不同的是,他們並不直接接觸系統底層,而是通過中間代碼訪問底層的。

中間代碼由誰處理呢,C#是通過.NET提供的CLR,產生的中間語言是程序集,而Java靠的是JVM,其中間產物是class文件。
如果有幸使用一些IDE打開這兩個文件往裡窺探一遭的話,我們應當不難從中找到這些元數據的信息。

這就好像,一群孩子進了幼兒園,一個託管老師全程進行看護。

把拔碼麻區上辦,我區悠貳園吶

當然,託管老師肯定是知道孩子叫什麼名的,訪問他們自然也是很容易的。同理,託管環境(或虛擬環境)也是一樣的,因為銜接上下兩層,因此把底層的元數據和上層的可讀文本構造反射的橋樑是很容易辦到的,因此,C#和Java都提供了一套非常完善的反射庫,他們可以被用於使用這兩種語言寫的任意一個類當中。

好了,道理我都懂,但為什麼要反射呢?

反射能幹什麼呢

舉個最簡單的例子

我……我有一個夢想,我想要這樣一個函數,能夠返回Person類是否有我所說的方法,但是我不知道Person類里有什麼,比如我想問他有沒有Eat()方法,它返回true,我問他有沒有Fly()方法,它能返回false

好了,換作是你,你會怎麼實現這樣一個函數呢??

而反射機制恰恰做到了!
你提供給反射機制一個字符串形式的函數名,反射機制不僅可以得知這個函數是否存在,甚至能幫助你去執行這個函數(Invoke)。

什麼,你不好問它有沒有某個函數??好啊,反射機制甚至可以告訴你這個類都有哪些屬性哪些函數,繼承自誰,可見性如何,是否抽象等等。

那反射在什麼時候比較好用呢

上面那個例子其實就是一個經典的用途。

或者,我們可以考慮另外一個場景。

你寫了某個函數接受了一個抽象為Object的對象,你希望,如果Object的對象存在方法Grow則調用之,否則什麼也不做。

這個時候首先可以通過反射機制確定方法是否存在,但即便方法已經存在,我們是無法直接調用的,因為對象已經抽象為Object,而Object並不存在方法Grow,所以直接調用就洗洗睡了。

我們不能具象回來么??

如果我們知道類在抽象之前是什麼類型的時候,那當然可以具象化回來。
但是抽象雖然發生於編譯時或運行時(動態創建的對象),但具象類型的獲知卻是在編譯之前的代碼源文件,而且還有些時候你根本無法知道原類型,那也沒辦法拆箱。


這裏面我為了方便,也是想不出啥更好的詞
這裏我稱派生類基類的多態轉化為抽象
反過來的過程稱為具象

那我還怎麼調用Grow

反射機制可以獲取到完整的可用方法的列表,我們在列表中找到了Grow,存在形式為Method/MethodInfo對象或乾脆就是個字符串。

但無論是哪種,obj.Grow();是不可能了,好在反射機制連這件事都考慮在內了——Invoke調用!!

反射機制不僅知道你想要什麼方法,還可以幫助你調用這個方法,這個調用就通過一個叫做Invoke的方法完成。

不同語言對Invoke的定義不盡相同但功能上大同小異,通過Invoke調用某方法的過程實質上是轉調回調(或者是間接調用)。
間接調用比直接調用更加的強大靈活,但繞了遠路。

還有什麼比較宏大一點的應用么??

宏大一點……好吧,其實每一個磅礴的工程都是從一點一滴做起的。

一個很經典的案例,就是上文那篇大佬文章里的一個常用功能——序列化(Serialization)
雖然C#和Java本身就有可以用於序列化的一些結構和功能庫(Serializable接口之類的),但是有些時候我們對序列化機制如果有更高的可定製性要求的時候,我們往往傾向於自己構建一套心儀的序列化功能庫。

於是乎就有一個最簡單的問題擺在面前:

現在有Class1類的對象,還有Class2類的對象,還有Class3類的一些對象想要轉化成可解析的內容,以供發送或保存(當然這也就意味着,這些對象的所有屬性和狀態都要保存),但是這老大老二老三一家一個樣,屬性也各不相同,我又不想挨個單獨寫,那該怎麼辦呢??

現在有了反射機制,問題就很容易解決了。三胞胎嫌分起來麻煩??反射機制可以把他們安排的明明白白!!你可以向反射類提供一個完整的類名,反射機制就能保證給你這個類對應的可用屬性的列表,以及一整套處理方案(Get和Set),之後還不是想來啥來啥,美滋滋~~

當然,以上都是反射機制用途中小的不能再小的冰山一角,比如我還可以通過反射機制根據我的輸入創建我想要的類型的對象等等。

哇,反射這麼強大??我要滿地反射!!

冷靜點!任何事物都有多面性,反射也不例外,我們看看反射機制有什麼特點,它到底是否適合所有情形。

極致靈動(Flexi Frenzy) 稀有屬性

反射機制可以讓你的代碼非常靈活,以不變應萬變。
這也正是反射機制帶來的最大的好處。

未卜先知(Fortune Tell) 普通屬性

反射機制是在運行時起作用,當然,運行期間發生什麼,編譯之前是無法獲知的,反射就是處理這件事的。

效率捉急(Emaciated Efficiency) 糟糕屬性

反射機制最大的問題!

反射機制的效率是十分低下的,首先在運行時獲取元數據再轉化成可讀形式就不是一個很快的過程,而反射的Invoke調用是個不折不扣的間接調用。

不當地使用大量反射會導致程序效率的急劇下降。

代碼膨脹(Code Expansion)

顯然,用反射進行調用的代碼往往比直接調用寫起來複雜,所以除非你寫代碼是按行數計工資,否則能直接調用就不要反射。

健壯風險(Robustness Risk)

反射機制一般允許用戶傳入字符串……

然後就是萬劫不復深淵之伊始

這時候用戶傳的字符串就可以非常的五花八門了,就好像一個動物園裡,反射機制是一個可愛的小動物,而遊客開始不分青紅皂白地對它投各種食,良莠不齊,可是你的反射機制很脆弱,它可禁不起這折騰,吃到不好的東西就會生病罷工(拋異常,然後中止),因此你這當奶媽奶爸就要多操心,幫它收拾(捕獲),告訴他如何分辨食物(預先判斷)……

不過呢,有些時候引入反射機制恰恰就是出於健壯性的考慮……

如果我養的不是個反射機制而是一隻熊貓的話我會上天的!!

總結

反射是個強大的武器,但使用應多加謹慎!

以上

【精選推薦文章】

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

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

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

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

詳解二分查找算法

我周圍的人幾乎都認為二分查找很簡單,但事實真的如此嗎?二分查找真的很簡單嗎?並不簡單。看看 Knuth 大佬(發明 KMP 算法的那位)怎麼說的:

Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…

這句話可以這樣理解:思路很簡單,細節是魔鬼。

本文就來探究幾個最常用的二分查找場景:尋找一個數、尋找左側邊界、尋找右側邊界。

而且,我們就是要深入細節,比如while循環中的不等號是否應該帶等號,mid 是否應該加一等等。分析這些細節的差異以及出現這些差異的原因,保證你能靈活準確地寫出正確的二分查找算法。

一、二分查找的框架

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = (right + left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

分析二分查找的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。本文都會使用 else if,旨在講清楚,讀者理解后可自行簡化。

其中…標記的部分,就是可能出現細節問題的地方,當你見到一個二分查找的代碼時,首先注意這幾個地方。後文用實例分析這些地方能有什麼樣的變化。

另外聲明一下,計算 mid 時需要技巧防止溢出,建議寫成: mid = left + (right – left) / 2,本文暫時忽略這個問題。

二、尋找一個數(基本的二分搜索)

這個場景是最簡單的,可能也是大家最熟悉的,即搜索一個數,如果存在,返回其索引,否則返回 -1。

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意

    while(left <= right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}

1. 為什麼 while 循環的條件中是 <=,而不是 < ?

答:因為初始化 right 的賦值是 nums.length – 1,即最後一個元素的索引,而不是 nums.length。

這二者可能出現在不同功能的二分查找中,區別是:前者相當於兩端都閉區間 [left, right],後者相當於左閉右開區間 [left, right),因為索引大小為 nums.length 是越界的。

我們這個算法中使用的是 [left, right] 兩端都閉的區間。這個區間就是每次進行搜索的區間,我們不妨稱為「搜索區間」(search space)

什麼時候應該停止搜索呢?當然,找到了目標值的時候可以終止:

    if(nums[mid] == target)
        return mid; 

但如果沒找到,就需要 while 循環終止,然後返回 -1。那 while 循環什麼時候應該終止?搜索區間為空的時候應該終止,意味着你沒得找了,就等於沒找到嘛。

while(left <= right)的終止條件是 left == right + 1,寫成區間的形式就是 [right + 1, right],或者帶個具體的数字進去 [3, 2],可見這時候搜索區間為空,因為沒有数字既大於等於 3 又小於等於 2 的吧。所以這時候 while 循環終止是正確的,直接返回 -1 即可。

while(left < right)的終止條件是 left == right,寫成區間的形式就是 [right, right],或者帶個具體的数字進去 [2, 2],這時候搜索區間非空,還有一個數 2,但此時 while 循環終止了。也就是說這區間 [2, 2] 被漏掉了,索引 2 沒有被搜索,如果這時候直接返回 -1 就可能出現錯誤。

當然,如果你非要用 while(left < right) 也可以,我們已經知道了出錯的原因,就打個補丁好了:

//...
while(left < right) {
    // ...
}
return nums[left] == target ? left : -1;

2. 為什麼 left = mid + 1,right = mid – 1?我看有的代碼是 right = mid 或者 left = mid,沒有這些加加減減,到底怎麼回事,怎麼判斷?

答:這也是二分查找的一個難點,不過只要你能理解前面的內容,就能夠很容易判斷。

剛才明確了「搜索區間」這個概念,而且本算法的搜索區間是兩端都閉的,即 [left, right]。那麼當我們發現索引 mid 不是要找的 target 時,如何確定下一步的搜索區間呢?

當然是去搜索 [left, mid – 1] 或者 [mid + 1, right] 對不對?因為 mid 已經搜索過,應該從搜索區間中去除。

3. 此算法有什麼缺陷?

答:至此,你應該已經掌握了該算法的所有細節,以及這樣處理的原因。但是,這個算法存在局限性。

比如說給你有序數組 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,沒錯。但是如果我想得到 target 的左側邊界,即索引 1,或者我想得到 target 的右側邊界,即索引 3,這樣的話此算法是無法處理的。

這樣的需求很常見。你也許會說,找到一個 target 索引,然後向左或向右線性搜索不行嗎?可以,但是不好,因為這樣難以保證二分查找對數級的時間複雜度了。

我們後續的算法就來討論這兩種二分查找的算法。

三、尋找左側邊界的二分搜索

直接看代碼,其中的標記是需要注意的細節:

int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意

    while (left < right) { // 注意
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return left;
}

1. 為什麼 while(left < right) 而不是 <= ?

答:用相同的方法分析,因為初始化 right = nums.length 而不是 nums.length – 1 。因此每次循環的「搜索區間」是 [left, right) 左閉右開。

while(left < right) 終止的條件是 left == right,此時搜索區間 [left, left) 恰巧為空,所以可以正確終止。

2. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:因為要一步一步來,先理解一下這個「左側邊界」有什麼特殊含義:

對於這個數組,算法會返回 1。這個 1 的含義可以這樣解讀:nums 中小於 2 的元素有 1 個。

比如對於有序數組 nums = [2,3,5,7], target = 1,算法會返回 0,含義是:nums 中小於 1 的元素有 0 個。如果 target = 8,算法會返回 4,含義是:nums 中小於 8 的元素有 4 個。

綜上可以看出,函數的返回值(即 left 變量的值)取值區間是閉區間 [0, nums.length],所以我們簡單添加兩行代碼就能在正確的時候 return -1:

while (left < right) {
    //...
}
// target 比所有數都大
if (left == nums.length) return -1;
// 類似之前算法的處理方式
return nums[left] == target ? left : -1;

3. 為什麼 left = mid + 1,right = mid ?和之前的算法不一樣?

答:這個很好解釋,因為我們的「搜索區間」是 [left, right) 左閉右開,所以當 nums[mid] 被檢測之後,下一步的搜索區間應該去掉 mid 分割成兩個區間,即 [left, mid) 或 [mid + 1, right)。

4. 為什麼該算法能夠搜索左側邊界?

答:關鍵在於對於 nums[mid] == target 這種情況的處理:

    if (nums[mid] == target)
        right = mid;

可見,找到 target 時不要立即返回,而是縮小「搜索區間」的上界 right,在區間 [left, mid) 中繼續搜索,即不斷向左收縮,達到鎖定左側邊界的目的。

5. 為什麼返回 left 而不是 right?

答:返回left和right都是一樣的,因為 while 終止的條件是 left == right。

四、尋找右側邊界的二分查找

尋找右側邊界和尋找左側邊界的代碼差不多,只有兩處不同,已標註:

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;

    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return left - 1; // 注意

1. 為什麼這個算法能夠找到右側邊界?

答:類似地,關鍵點還是這裏:

    if (nums[mid] == target) {
        left = mid + 1;

當 nums[mid] == target 時,不要立即返回,而是增大「搜索區間」的下界 left,使得區間不斷向右收縮,達到鎖定右側邊界的目的。

2. 為什麼最後返回 left – 1 而不像左側邊界的函數,返回 left?而且我覺得這裏既然是搜索右側邊界,應該返回 right 才對。

答:首先,while 循環的終止條件是 left == right,所以 left 和 right 是一樣的,你非要體現右側的特點,返回 right – 1 好了。

至於為什麼要減一,這是搜索右側邊界的一個特殊點,關鍵在這個條件判斷:

    if (nums[mid] == target) {
        left = mid + 1;
        // 這樣想: mid = left - 1

因為我們對 left 的更新必須是 left = mid + 1,就是說 while 循環結束時,nums[left] 一定不等於 target 了,而 nums[left – 1]可能是target。

至於為什麼 left 的更新必須是 left = mid + 1,同左側邊界搜索,就不再贅述。

3. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:類似之前的左側邊界搜索,因為 while 的終止條件是 left == right,就是說 left 的取值範圍是 [0, nums.length],所以可以添加兩行代碼,正確地返回 -1:

while (left < right) {
    // ...
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;

五、最後總結

先來梳理一下這些細節差異的因果邏輯:

第一個,最基本的二分查找算法:

因為我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因為我們只需找到一個 target 的索引即可
所以當 nums[mid] == target 時可以立即返回

第二個,尋找左側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最左側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊右側邊界以鎖定左側邊界

第三個,尋找右側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界

又因為收緊左側邊界時必須 left = mid + 1
所以最後無論返回 left 還是 right,必須減一

如果以上內容你都能理解,那麼恭喜你,二分查找算法的細節不過如此。

通過本文,你學會了:

1. 分析二分查找代碼時,不要出現 else,全部展開成 else if 方便理解。

2. 注意「搜索區間」和 while 的終止條件,如果存在漏掉的元素,記得在最後檢查。

3. 如需要搜索左右邊界,只要在 nums[mid] == target 時做修改即可。搜索右側時需要減一。

就算遇到其他的二分查找變形,運用這幾點技巧,也能保證你寫出正確的代碼。LeetCode Explore 中有二分查找的專項練習,其中提供了三種不同的代碼模板,現在你再去看看,很容易就知道這幾個模板的實現原理了。

【精選推薦文章】

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

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

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

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

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

用象棋的思維趣說IT人的職業發展和錢途,在上海軟件行業,我見到的年入50萬的眾生相——我們該如何努力從而達到這個目標

    最近我花了不少功夫在學習象棋,也學習了王天一等高手的棋路,感覺IT人的職業和下棋一樣,往好了講,爭主動權爭實惠只爭朝夕,往壞了講,一步走錯得用多步來彌補,如果錯誤太大未必能彌補回來。在本文里,就用下棋這種比較輕鬆的話題來聊聊程序員該如何經營自己的職業。

1 對手是誰,如何算輸贏

    做任何事情都要明確對象和目標,下棋時,對手和目標都很明確,但在職業規劃里,對手是誰?如何算輸贏?

    對手從小了講,是自己,畢竟除山中賊易,除心中賊難,往大了講,是其它IT人。

    如何算輸贏?這有很多標準,往大了講,達到財務自由或者“掙到一個億”算贏,往中了講,有房有車有孩子,或者達到中產水平,往小了講,能在社會上立足,收入在平均水平以上。在本文里,不說過於遠大的目標,倒也不是遙不可及,而是再遠大的目標也是得靠腳踏實地,也不說“收入超平均水平”,畢竟這還是比較容易的,就算“達到中產水平”為贏。

    再具體點,如何算中產?我找了上海2018年中產的標準如下。

    1,有兩套及以上住房(不含和父母共有的),至少一套達非普標準;

    2,沒有房貸,或各項負債合計低於總資產30%。

    3,家庭年收入(不含父母)達50萬以上。

    之前本人寫過篇博文,在上海軟件行業,我見到的年入50萬的眾生相——我們該如何努力從而達到這個目標,年入50萬也算是中產吧。 

2 少走緩手棋

    一盤象棋有60個回合不算長,算下從大學畢業到退休,大致有30年吧,大概平攤下來,半年時間能算一個回合,也就是說,用半年的努力,自己職業上應該有顯著的效果。

    下棋時不怕局勢一般,就怕下緩手棋,高手之間過招,2步緩手足以輸棋,一般的人下棋,也經不起3步以上的緩手。

    那麼職業乃至人生的棋盤上,什麼叫緩手呢?

    1 在公司里得過且過,不主動追求技術進步,導致半年左右技術等方面沒進步。

    2 自己的技能已經得到提升,但出於安逸,沒有追求更高的工資或者更高級的職位。

    3 當工資收入達到一定水準后,不思進取,不積極探索新的掙錢渠道和掙錢模式。

    那麼在工作中,一旦下緩手棋會有什麼後果呢?

    1 半年不進步,看似沒什麼影響,但由奢入儉難,一旦得過且過的日子過慣了,再想上進就難了。

    2 如果一年不進步,技術馬上會落後於平均水平。

    3 一年半以上的不進步,就不說跳槽難了,這已經能導致在項目組裡日子難過(因為別人都在進步),更有甚者,如果公司動蕩,這批混日子而且技術不行的人,估計會第一批被淘汰,淘汰后甚至後面工作也難找。 

3 只爭朝夕,爭主動權更得靠效率

    和“少走緩手棋”相對的是“力爭主動權”。

    象棋里主動權直觀表現為,我可以從容調度子力進攻,能從容圍剿對方的子力從而得到子力優勢。在職業規劃里,主動權表現為,我不僅能從容應付當前的工作,而且由於我的技術以及能力在不斷提升,後繼我的技術發展方向和公司選型,我都掌握主動。從實惠角度來講,增加收入的主動權在自己手裡,在當前公司,我可以通過晉陞提升收入,我也可以通過跳槽提升收入,還可以通過各種額外手段提升收入。

    象棋里爭奪主動權靠積極和效率,所謂用最小的“度數”(棋子的移動步數)把子力放置到(給對方造成威脅最大的)積極位置。平時工作中,爭奪主動權的方式也是如此。

    怎麼算積極呢?列下我見到的比較積極的大牛事例。    

    1 態度端正,一天當两天用,在工作中,也絕無看網頁等做和工作無關事情的情況,有問題也是馬上確認積極解決,絕無拖沓。

    2 盡可能地利用各種時間碎片幹活或提升技術,比如我公司里的大牛中午休息時間也在看技術文章,博客園裡的一些大牛也是利用早上上班前或下班回家后的時間寫技術博客,總之積極地盡一切可能提升自己。

    3 從來不停止提升自己,比如我之前互聯網公司的一位大牛,Spring方面在業內非常出名,但平時也時一有時間就看Spring文章,一有可能就出書出視頻教程。

    而提升工作效率除了不斷總結之外,還得盡可能減少不必要的休閑娛樂時間,比如我之前互聯網公司的多位大牛,都是手機上沒有遊戲app,也沒聽說過追劇。

    知易行難,其實道理也很通俗,做起來並不容易,我的體會是,在開始階段稍微逼下自己,比如早起寫博客,工作時盡量不看無關事,平時積極些,可能最多一個月,當形成習慣時,想改也難。 

4 勢在子先,首先得提升自己的掙錢能力

    象棋里有寧失一子,不失一先的說法。如果把象棋中的形勢比作個人技能綜合能力(把握主動權的能力),那麼“子力”就好比掙到的錢。

    也就是說,能力比錢重要,一方面我們應該踏踏實實地提升能力,排除短期跳槽帶來的金錢誘惑,另一方面,在必要時,可以用錢買技能,比如在必要的時候買收費專欄里的知識,甚至參加培訓班。

    我見到的反面例子是,在之前的外企里,某人本身能力也就一般,但還在外面干兼職,這就屬於要錢不要技能,結果短時間內錢確實掙到了,但由於不注意提升能力,最後被迫離開了項目組。而正面的例子就比較多了,不少人在進項目組時能力一般,但平時通過看各種(免費和收費)資料,最後成為項目組內的棟樑。

    再說下應該重視哪些能力?

    1 對底層代碼的理解以及分析能力。

    2 能在短時間內全局掌握系統架構的能力。

    3 比較值錢的,比如大數據機器學習和分佈式的技能。

    4 帶團隊的能力,比如對外能和客戶方溝通,在公司內能和其它組協調扯皮,在組內能合理分派活,合理監管質量同時保證進度。 

5 優勢情況下更不能大意

   下棋時,我經常遇到如下的場景,我或者對手優勢很明顯,但由於一步走錯,局勢逆轉,對此象棋有“臨殺誤急“和“贏棋不鬧事”的說法。

    大家在工作中,這種情況也不能說沒,比如發布項目,什麼工作都準備好了,看似勝利在望,但在發布時,由於大意導致操作事務,最後出現產線問題, 

    象棋在沒贏前,任何走法都得反覆斟酌,同樣在發布項目等工作沒通過驗證前,也不能放鬆,這不僅得到主觀上態度端正,更得“未慮勝,先慮敗” ,盡量把可能導致失敗的因素都考慮全,並針對性地做預案或補償措施。

    我見過不少下棋的人,只會一廂情願,或者沉溺於自己的優勢中,或者主觀上忽視對方的反擊手段,這種人下棋贏了是運氣,輸了是必然,而且這樣下能力也不會提升。同樣,在做項目等工作時,寧可做好針對最壞情況的準備,比如發布時做好,甚至操練好回退預案,寧可用不到。這樣的話,一方面能確保勝局,另一方由於考慮周全,技能上一定也會有提升。 

6 取勝得靠進攻手段,IT人掙錢也得積極

    象棋中取勝一般有兩種方法,一種是確保自己不犯錯然後坐等對方走錯,另一種是積極主動擴大主動權,積優勢為勝勢。

    對應IT人掙錢方式,一種是干好手頭的活,確保不出問題,然後循例升職和加工資,另一種則是積極擴大自己的品牌,不斷探索新的掙錢方式,相比之下,後者更加積極,掙錢效率應該也優於前者。

    正如特級大師的擴大先手和進攻手段得靠慢慢積累,IT人掙錢的方式也得從小做起。比如小到通過寫博客積累,然後可以開專欄出視頻教程,再大點可以通過各種渠道找些項目,再大些可以做自己得產品。

    說起來容易做起來難,但一旦有積極想法了,而不是坐等工資等穩定收入,其它不敢說,至少自己的格局能不斷變廣,掙錢的機會也會越來越多。 

7 逆勢中不能自暴自棄,只要努力,就有翻盤可能

    下棋時不可能老是贏,有不少時候局面會很難看,只能苦苦支撐。相比之下,工作中也不可能一帆風順,比如工作氛圍不好比較壓抑,有996工作壓力大,或者錢少,這種時候絕不能自暴自棄。

    特級大師之間,出現翻盤的案例也不少,更何況業餘的,比如有10盤逆勢棋,如果自暴自棄亂下,估計最多一兩盤能下翻盤,但如果咬牙堅持每步都下好,翻盤的可能性就大很多。

    對應的在工作中,哪怕情況再差,每天也得儘力做好自己的事,必要時更得提升自己的能力,指不定哪天就時來運轉了,或者這個公司沒機會,由於自己一刻也沒放鬆反而在不斷提升自己的實力,在下家公司里就能春風得意了。

8 總結:人生棋局裡,一時輸贏不能代表今後的輸贏

     大家經常能看到,某人前幾年由於混日子,活得沒指望,但突然想明白了,只用了半年就完成了逆轉,也就是說,雖然一步走錯需要用更大的代價來彌補,但一時的輸贏絕不能代表一世的得失。所以,在任何時候都得積極向上力爭主動。本文也算戲說,大家在一笑之餘如果感到有意思,或者有收穫,請幫忙推薦下本文,謝謝大家。

版權說明:

    有不少網友轉載和想要轉載我的博文,本人感到十分榮幸,這也是本人不斷寫博文的動力。關於本文的版權有如下統一的說明,抱歉就不逐一回復了。

    1 本文可轉載,無需告知,轉載時請用鏈接的方式,給出原文出處,別簡單地通過文本方式給出,同時寫明原作者是hsm_computer。

    2 在轉載時,請原文轉載 ,謝絕洗稿。否則本人保留追究法律責任的權利。

【精選推薦文章】

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

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

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

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

附004.Kubernetes Dashboard簡介及使用

一 Kubernetes dashboard簡介

1.1 Web UI簡介

dashboard是基於Web的Kubernetes用戶界面。可以使用dashboard將容器化應用程序部署到Kubernetes集群,對容器化應用程序進行故障排除,以及管理集群資源。可以使用dashboard來概述群集上運行的應用程序,以及創建或修改單個Kubernetes資源(例如部署、任務、守護進程等)。可以使用部署嚮導擴展部署,啟動滾動更新,重新啟動Pod或部署新應用程序。
dashboard還提供有關群集中Kubernetes資源狀態以及可能發生的任何錯誤的信息。

二 dashboard部署

2.1 下載yaml

  1 [root@master ~]# mkdir dashboard					#建議將yaml存儲到本地
  2 [root@master ~]# cd dashboard/
  3 [root@master dashboard]# wget https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml

 

2.2 修改為國內源

  1 [root@master ~]# cd dashboard/
  2 [root@master dashboard]# vi kubernetes-dashboard.yaml
  3 ……
  4 image: mirrorgooglecontainers/kubernetes-dashboard-amd64:v1.10.1
  5 ……

 
提示:將yaml文件中的image字段修改為mirrorgooglecontainers/kubernetes-dashboard-amd64:v1.10.1。

2.3 安裝

  1 [root@master dashboard]# kubectl apply -f kubernetes-dashboard.yaml
  2 [root@master ~]# kubectl get pod --all-namespaces -o wide| grep kubernetes-dashboard			#確認驗證
  3 kube-system	kubernetes-dashboard-68ddcc97fc-c5thv	0/1	Running	0	30s	<none>	node2	<none>	<none>

 

三 dashboard訪問方式

3.1 訪問方式概述

安裝dashboard后,需要為用戶配置對群集資源的訪問控制。從版本1.7開始,dashboard不再具有默認授予的完全管理員權限。默認所有權限都被撤銷,並且只授予了使Dashboard工作所需的最小權限。
提示:本說明僅針對使用Dashboard 1.7及更高版本的用戶。如果確信dashboard需要授予管理員權限,可參考:《附006.Kubernetes身份認證》
通常,其他應用程序不應直接訪問dashboard。
dashboard有以下三種訪問方式:
kubectl proxy:只能在localhost上訪問。訪問地址:http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
NodePort:編輯 kubernetes-dashboard.yaml文件中,將 type: ClusterIP 改為 type: NodePort,確認dashboard運行在哪個節點后。訪問地址:https://<node-ip>:<nodePort>
apiserver:需要在瀏覽器中安裝用戶證書。訪問地址: https://<master-ip>:<apiserver-port>/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
提示:kubectl proxy方式不推薦,建議使用有效證書來建立安全的HTTPS連接。

3.2 kubectl proxy

  1 [root@master ~]# kubectl proxy
  2 [root@master ~]# curl http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/	#訪問

 
提示:建議通過後端形式,並且允許所有主機訪問的方式:

  1 [root@master ~]# nohup kubectl proxy --address='0.0.0.0' --accept-hosts='^*$' &

3.3 NodePort

NodePort訪問儀錶板的方式僅建議用於單節點設置中的Kubernetes環境。

  1 [root@master ~]# kubectl -n kube-system edit service kubernetes-dashboard
  2 ……
  3    type: NodePort
  4 ……
  5 #將type: ClusterIP  修改為 type: NodePort。

 
提示:以上操作也可通過以下命令一步完成:

  1 [root@master ~]# kubectl get pods --namespace=kube-system | grep dashboard
  2 kubernetes-dashboard-68ddcc97fc-c5thv   1/1     Running   0          3h14m
  3 [root@master ~]# kubectl describe pod kubernetes-dashboard-68ddcc97fc-c5thv --namespace=kube-system | grep Node
  4 Node:               node2/172.24.8.73

 
測試訪問:
瀏覽器訪問:http://172.24.8.73:30343/
提示:若部署的Kubernetes集群為多節點集群,需要通過以上方式查找dashboard所在的node節點,若為單節點集群,則直接訪問http://<master>:<port>即可。

3.4 apiserver

若Kubernetes API服務器公開並可從外部訪問,可瀏覽器直接訪問:https://172.24.8.71:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
注意:apiserver默認情況下使用system:anonymous用戶進行訪問,因此沒有權限打開相應資源。只有在選擇在瀏覽器中安裝用戶證書時,才能使用這種訪​​問儀錶板的方式。
NodePort和apiserver都需要配置相應的認證才可訪問,確定某種方式方式后需要配置認證類型。

3.5 Ingress

dashboard也可以使用ingress進行資源公開。
可參考:https://kubernetes.io/docs/concepts/services-networking/ingress/

四 dashboard驗證方式

瀏覽器訪問的時候需要加載相應證書,部署完成后默認已經創建相應證書,可參考附01進行導出。由於Kubernetes默認證書可能過期導致無法訪問dashboard,本實驗在已成功部署Kubernetes後手動重新創建證書。

4.1 創建證書

  1 [root@master ~]# mkdir /etc/kubernetes/dash_pki
  2 [root@master ~]# cd /etc/kubernetes/dash_pki/
  3 [root@master dash_pki]# openssl genrsa -out ca.key 2048				#生成一個 2048 bit 的 ca.key
  4 [root@master dash_pki]# openssl req -x509 -new -nodes -key ca.key -subj "/CN=172.24.8.71" -days 10000 -out ca.crt	                #根據 ca.key 生成一個 ca.crt(使用 -days 設置證書的有效時間)
  5 [root@master dash_pki]# openssl genrsa -out server.key 2048			        #生成一個 2048 bit 的 server.key
  6 [root@master dash_pki]# openssl req -new -key server.key -subj "/CN=172.24.8.71" -out server.csr			                #根據 server.key 生成一個 server.csr
  7 [root@master dash_pki]# openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 10000	#根據 ca.key、ca.crt 和 server.csr 生成 server.crt
  8 subject=/CN=172.24.8.71
  9 Getting CA Private Key
 10 [root@master dash_pki]# openssl x509  -noout -text -in ./server.crt		        #查看證書

 

4.2 修改默認證書配置

  1 [root@master ~]# cd dashboard/
  2 [root@master dashboard]# kubectl delete -f kubernetes-dashboard.yaml		#刪除使用默認證書所創建的dashboard
  3 [root@master dashboard]# ll /etc/kubernetes/dash_pki/
  4 [root@master dashboard]# kubectl create secret generic kubernetes-dashboard-certs --from-file="/etc/kubernetes/dash_pki/server.crt,/etc/kubernetes/dash_pki/server.key" -n kube-system	#掛載新證書到dashboard
  5 [root@master dashboard]# kubectl get secret kubernetes-dashboard-certs -n kube-system -o yaml	#查看新證書

 

4.3 重新部署dashboard

  1 [root@master dashboard]# kubectl apply -f kubernetes-dashboard.yaml
  2 [root@master dashboard]# kubectl get pods --namespace=kube-system | grep dashboard		#確認驗證

 

4.4 導入證書

將server.crt導入IE瀏覽器,導入操作略。

4.5 訪問測試

本試驗基於apiserver訪問方式+Kubeconfig身份驗證進行登錄。
通過apiserver形式訪問:https://172.24.8.71:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
提示:dashboard登錄整個流程可參考:https://www.cnadn.net/post/2613.htm
提示:apiserver方式見3.4,Kubeconfig驗證方式見《附006.Kubernetes身份認證》中的3.5。  

附001:導出當前Kubernetes證書

[root@master ~]# grep ‘client-certificate-data’ ~/.kube/config | head -n 1 | awk ‘{print $2}’ | base64 -d >> kubecfg.crt
[root@master ~]# grep ‘client-key-data’ ~/.kube/config | head -n 1 | awk ‘{print $2}’ | base64 -d >> kubecfg.key
[root@master ~]# openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out k8s.crt -name “kubernetes-client”
Enter Export Password:[x120952576]
Verifying – Enter Export Password:[x120952576]
使用相應的密碼,將k8s.crt導入IE瀏覽器,導入操作略。    
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=20stclch1nfo4

【精選推薦文章】

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

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

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

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