Prometheus 入門

簡介

Prometheus 是一套開源的系統監控報警框架。它啟發於 Google 的 borgmon 監控系統,由工作在 SoundCloud 的 google 前員工在 2012 年創建,作為社區開源項目進行開發,並於 2015 年正式發布。

特點

作為新一代的監控框架,Prometheus 具有以下特點:

  • 強大的多維度數據模型:
  1. 時間序列數據通過 metric 名和鍵值對來區分。
  2. 所有的 metrics 都可以設置任意的多維標籤。
  3. 數據模型更隨意,不需要刻意設置為以點分隔的字符串。
  4. 可以對數據模型進行聚合,切割和切片操作。
  5. 支持雙精度浮點類型,標籤可以設為全 unicode。
  • 靈活而強大的查詢語句(PromQL):在同一個查詢語句,可以對多個 metrics 進行乘法、加法、連接、取分數位等操作。
  • 易於管理: Prometheus server 是一個單獨的二進制文件,可直接在本地工作,不依賴於分佈式存儲。
  • 高效:平均每個採樣點僅占 3.5 bytes,且一個 Prometheus server 可以處理數百萬的 metrics。
    使用 pull 模式採集時間序列數據,這樣不僅有利於本機測試而且可以避免有問題的服務器推送壞的 metrics。
  • 可以採用 push gateway 的方式把時間序列數據推送至 Prometheus server 端。
  • 可以通過服務發現或者靜態配置去獲取監控的 targets。
  • 有多種可視化圖形界面。
  • 易於伸縮。

組成及架構

Prometheus 生態圈中包含了多個組件,其中許多組件是可選的:

  • Prometheus Server: 用於收集和存儲時間序列數據。
  • Client Library: 客戶端庫,為需要監控的服務生成相應的 metrics 並暴露給 Prometheus server。當 Prometheus server 來 pull 時,直接返回實時狀態的 metrics。
  • Push Gateway: 主要用於短期的 jobs。由於這類 jobs 存在時間較短,可能在 Prometheus 來 pull 之前就消失了。為此,這次 jobs 可以直接向 Prometheus server 端推送它們的 metrics。這種方式主要用於服務層面的 metrics,對於機器層面的 metrices,需要使用 node exporter。
  • Exporters: 用於暴露已有的第三方服務的 metrics 給 Prometheus。
  • Alertmanager: 從 Prometheus server 端接收到 alerts 后,會進行去除重複數據,分組,並路由到對收的接受方式,發出報警。常見的接收方式有:电子郵件,pagerduty,OpsGenie, webhook 等。
  • 一些其他的工具。

下圖為 Prometheus 官方文檔中的架構圖:

從上圖可以看出,Prometheus 的主要模塊包括:Prometheus server, exporters, Pushgateway, PromQL, Alertmanager 以及圖形界面。

其大概的工作流程是:

  1. Prometheus server 定期從配置好的 jobs 或者 exporters 中拉 metrics,或者接收來自Pushgateway 發過來的 metrics,或者從其他的 Prometheus server 中拉 metrics。
  2. Prometheus server 在本地存儲收集到的 metrics,並運行已定義好的 alert.rules,記錄新的時間序列或者向 Alertmanager 推送警報。
  3. Alertmanager 根據配置文件,對接收到的警報進行處理,發出告警。
    在圖形界面中,可視化採集數據。

相關概念

下面將對 Prometheus 中的數據模型(時間序列),metric 類型,instance 和 jobs等概念進行介紹。

數據模型

Prometheus 中存儲的數據為時間序列,是由 metric 的名字和一系列的標籤(鍵值對)唯一標識的,不同的標籤則代表不同的時間序列。

  • metric 名字:該名字應該具有語義,一般用於表示 metric 的功能,例如:http_requests_ total, 表示 http 請求的總數。其中,metric 名字由 ASCII 字符,数字,下劃線,以及冒號組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 標籤:使同一個時間序列有了不同維度的識別。例如 http_requests_total{method=”Get”} 表示所有 http 請求中的 Get 請求。當 method=”post” 時,則為新的一個 metric。標籤中的鍵由 ASCII 字符,数字,以及下劃線組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 樣本:實際的時間序列,每個序列包括一個 float64 的值和一個毫秒級的時間戳。
  • 格式: { =, …},例如:http_requests_total{method=”POST”,endpoint=”/api/tracks”}。

Metrics種類

Prometheus客戶端庫提供了四種核心Metrics類型。

Counter(計數器)

  • 說明:Counter是一個累積度量,它表示一個單調遞增的 Metrics,其值只能在重啟時遞增或重置為零
  • 場景:可以使用Counter來表示http的請求數、已完成的任務數或錯誤數、下單數。

Gauge(測量儀)

  • 說明:當前值的一次快照(snapshot)測量,可增可減。
  • 場景:磁盤使用率,當前同時在線用戶數。

Histogram(直方圖)

  • 說明:通過區間統計樣本分佈。
  • 場景:請求延遲時間的統計。例如統計 0~200ms、200ms~400ms、400ms~800ms 區間的請求數有多。

Summary(匯總)

  • 說明:根據樣本統計出百分位。
  • 場景:請求延遲時間的統計。例如統計 95%的請求延遲 < xxx ms ,99%的請求延遲 < xxx ms

instance 和 jobs

在Prometheus術語中,你可以scrape(刮擦)的端點稱為 實例,通常對應於單個進程。一組同種類型的 instances(主要用於保證可擴展性和可靠性),例如:具有四個複製instances(實例)的API服務器job作業:

  • job: api-server
    • instance 1: 1.2.3.4:5670
    • instance 2: 1.2.3.4:5671
    • instance 3: 5.6.7.8:5670
    • instance 4: 5.6.7.8:5671

當Prometheus scrape(刮擦)目標時,它會自動在scrape的時間序列上附加一些標籤,用來識別scrape的目標。

  • job:目標所屬的已配置job名稱。
  • instance: : 已刮擦的目標URL 的一部分。

對於每次實例 scrape(刮取,Prometheus都會在以下時間序列中存儲樣本:

  • up{job=”<job-name>”, instance=”<instance-id>”}:1如果實例是健康的,即可達,或者0刮擦失敗。
  • scrape_duration_seconds{job=”<job-name>”, instance=”<instance-id>”}:刮擦持續時間。
  • scrape_samples_post_metric_relabeling{job=”<job-name>”, instance=”<instance-id>”}:應用度量標準重新標記后剩餘的樣本數。
  • scrape_samples_scraped{job=”<job-name>”, instance=”<instance-id>”}:目標暴露的樣本數。
  • scrape_series_added{job=”<job-name>”, instance=”<instance-id>”}:該刮擦中新系列的大致數量。v2.10中的新功能。

up時間序列對於實例可用性監視非常有用。

安裝和配置

安裝

你可以在官網 https://prometheus.io/download/ 下載 安裝包,解壓后使用。為了方便,我使用docker 鏡像的方式 運行Prometheus。

docker run --name prometheus -d -p 9090:9090 prom/prometheus

瀏覽器輸入http://localhost:9090 ,訪問 Prometheus 的 Web UI:

點擊菜單欄 “Status” 下的 Targets ,界面如下:

可以看大Prometheus 自身 metrics 處於UP狀態 ,說明 安裝成功。

配置

Prometheus 的配置文件 prometheus.yml 內容如下:

# 全局設置,可以被覆蓋
global:
  scrape_interval:     15s
  evaluation_interval: 15s
  
rule_files:
  # - "first.rules"
  # - "second.rules"

scrape_configs:
  - job_name: prometheus
    static_configs:
    - targets: ['localhost:9090']

global塊控制 Prometheus 的全局配置。我們有兩種選擇。第一個,scrape_interval控制Prometheus 刮擦目標的頻率。你可以為單個目標覆蓋此值。在這種情況下,全局設置是每15秒刮一次。該evaluation_interval選項控制普羅米修斯評估規則的頻率。Prometheus 使用規則創建新的時間序列並生成警報。

rule_files塊指定我們希望 Prometheus 加載的任何規則的位置。現在我們沒有規則。

最後一個塊scrape_configs控制 Prometheus 監視的資源。由於 Prometheus 還將自己的數據公開為HTTP端點,因此它可以抓取並監控自身的健康狀況。在默認配置中有一個名為 prometheus 的job,它抓取 prometheus 服務器 公開的時間序列數據。該作業包含一個靜態配置的目標,即端口9090上的本地主機。返回的時間序列數據將詳細說明Prometheus服務器的狀態和性能。

實驗

Prometheus HTTP 度量模擬器

為了演示 Prometheus 的簡單使用,這裏運行一個 Prometheus HTTP 度量模擬器。模擬一個簡單的HTTP微服務,生成Prometheus Metrics,通過 docker 運行。

docker run -p 8080:8080 pierrevincent/prom-http-simulator:0.1

它在/metrics端點下公開以下Prometheus指標:

  • http_requests_total:請求計數器,標籤endpoint和status
  • http_request_duration_milliseconds:請求延遲直方圖

可以開啟流量高峰模式,更改流量高峰模式可以通過以下方式完成:

# ON
curl -X POST http://127.0.0.1:8080/spike/on

# OFF
curl -X POST http://127.0.0.1:8080/spike/off

# RANDOM
curl -X POST http://127.0.0.1:8080/spike/random

錯誤率默認為1%。它可以更改為0到100之間的数字:

# 例如將錯誤率設置為50%
curl -H 'Content-Type: application/json' -X PUT -d '{"error_rate": 50}' http://127.0.0.1:8080/error_rate

修改Prometheus配置

需要將 HTTP 度量模擬器 的 metrics端點 配置到 Prometheus的配置文件 prometheus.yml 中。

創建一個 prometheus.yml 文件 內容如下:

global:
  scrape_interval: 5s
  evaluation_interval: 5s
  scrape_timeout: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']
  - job_name: 'http-simulator'
    metrics_path: /metrics
    static_configs:
    - targets: ['172.16.1.232:8080']

通過docker up 命令替換 容器中的配置文件:

docker cp prometheus.yml prometheus:/etc/prometheus/

重啟容器:

docker restart prometheus

訪問 http://localhost:9090/targets ,發現已經出現了 target “http-simulator” ,並且為UP狀態。

查詢

請求率(Request Rate)查詢

查詢http請求數

http_requests_total{job="http-simulator"}

查詢成功login請求數

http_requests_total{job="http-simulator", status="200", endpoint="/login"}

查詢成功請求數,以endpoint區分

http_requests_total{job="http-simulator", status="200"}

查詢總成功請求數

sum(http_requests_total{job="http-simulator", status="200"})

查詢成功請求率,以endpoint區分

rate(http_requests_total{job="http-simulator", status="200"}[5m])

查詢總成功請求率

sum(rate(http_requests_total{job="http-simulator", status="200"}[5m]))

延遲分佈(Latency distribution)查詢

查詢http-simulator延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator"}

查詢成功login延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}

不超過200ms延遲的成功login請求佔比

sum(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login", le="200"}) / sum(http_request_duration_milliseconds_count{job="http-simulator", status="200", endpoint="/login"})

成功login請求延遲的99百分位

histogram_quantile(0.99, rate(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}[5m]))

上面給出的這些查詢表達式,在 prometheus 的 查詢界面上自行測試下 ,這裏就不一一測試了,

總結

本篇對 Prometheus 的組成,架構和基本概念進行了介紹,並實例演示了 Prometheus 的查詢表達式的應用。本篇是 Prometheus 系列的第一篇, 後續還會有Prometheus與其他圖形界面的集成,與 springboot 應用的集成等 。

參考

https://prometheus.io/docs/introduction/overview/
https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-prometheus-getting-started-and-practice/index.html

歡迎掃碼或微信搜索公眾號《程序員果果》關注我,關注有驚喜~

【精選推薦文章】

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

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

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

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

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

Trie|如何用字典樹實現搜索引擎的關鍵詞提示功能

Trie字典樹

Trie字典樹又稱前綴樹,顧名思義,是查詢前綴匹配的一種樹形數據結構

可以分為插入(創建) 和 查詢兩部分。參考地址極客時間

下圖為插入字符串的過程:

創建完成后,每個字符串最後一個字母標記為終結點(圖中显示為紅色)

下圖為查詢字符串:“her”的過程:綠色箭頭表示查詢路徑
我們將要查找的字符串分割成單個的字符 h,e,r,一個一個查詢

下圖為查詢字符串:“he”的過程:綠色箭頭表示查詢路徑
因為‘e’不是終結點,所以不能完全匹配上。

Trie字典樹的實現

1.首先是字典樹 數據結構定義的代碼實現

樹形結構,類比於二叉樹的存儲嘛,每個結點兩條分支(二叉樹);
而字典樹,每個節點可以最多有 26個分支(存儲英文字母)。

1-1二維數組存儲字母

int trie[MAX_NODE][26];//MAX_NODE表示結點數量,每個結點有26個字母結點
int k;

MAX_NODE表示結點數量,每個結點有26個字母結點
Trie[i][j]的值是0,表示trie樹中i號節點,並沒有一條連出去的邊滿足邊上的字符標識是字符集中第j個字符(從0開始);
trie[i][j]的值是正整數x表示trie樹中i號節點,有一條連出去的邊滿足邊上的字符標識是字符集中第j個字符,並且
這條邊的終點是x號節點。

1-2鏈表
我這裏用C++中的vector實現,

vector< pair<char, int> > trie[MAX_NODE];
int k;

也可以寫一個真正的鏈表,包含二元組字段<char,int>型的對應關係

1-3hash,

map<char, int> trie[MAX_NODE];

每次我們想找i號節點有沒有標識
是某個字符ch的邊時,只要看trie[i][ch]的值即可
但是實際上map時空複雜度的常數都比較大

2.插入 和 查詢 兩個函數的代碼實現

插入 查詢 實際上是類似的,就是從樹的根開始往下遍歷,

2-1插入:從樹的根開始往下遍歷,到達一個結點,沒有這個字母就插入到這個結點下,作為這個結點的子節點

基於二維數組結構的插入功能實現

代碼的第6~8行,一開始trie[][]被初始化為0,保證每個節點被創建出來時,都沒有子節點。K初
始化為1表示一開始只有1個節點,也就是0號節點根節點。Color是用來標記一個節點是不是終結
點。Color[i]=1標識i號節點是終結點。
第9~21行是插入函數insert(w),w是字符指針,實際上可以看作是一個字符串。
第11行是p從0號節點開始。
第12~19行是依次插入w的每一個字符。
第13行是計算w[i]是字符集第幾個字符,這裏我們假設字符集只包含26個小寫字母。
第14~17行是如果p沒有連出標識是w[i]的邊,那麼就創建一個。這裏新創建的節點一定就是k號節
點。所謂創建新節點實際上也沒什麼可創建的,新節點就是個編號。所以我們直接令trie[i][c]=k
即可,然後將k累加1,整個創建過程就完成了。
第18行是沿着標記着w[i]的邊移動到下一個節點。
最後第20行,是將最後到達的節點p標記為終結點。

2-2查詢:從樹的根開始往下遍歷,查看是否匹配上當前正在查的單詞
基於二維數組結構的查詢功能實現

第24行是從p=0也就是根節點開始。
第25~29行是枚舉s的每一個字符。
第26行是計算當前字符s[i]在字符集的序號。
第27行是判斷p節點有沒有連出標識s[i]字符的邊,如果沒有,說明現在無路可走,直接返回0;如
果有的話,
第28行就是移動到下一個節點。如果整個循環結束還沒有return 0,那就說明成功沿着s的每一個
字符到達了p節點。這時只要判斷p節點是不是終結點即可,也就是第30行的代

3.完整代碼C++版

public class Trie {
  private TrieNode root = new TrieNode('/'); // 存儲無意義字符

  // 往 Trie 樹中插入一個字符串
  public void insert(char[] text) {
    TrieNode p = root;
    for (int i = 0; i < text.length; ++i) {
      int index = text[i] - 'a';
      if (p.children[index] == null) {
        TrieNode newNode = new TrieNode(text[i]);
        p.children[index] = newNode;
      }
      p = p.children[index];
    }
    p.isEndingChar = true;
  }

  // 在 Trie 樹中查找一個字符串
  public boolean find(char[] pattern) {
    TrieNode p = root;
    for (int i = 0; i < pattern.length; ++i) {
      int index = pattern[i] - 'a';
      if (p.children[index] == null) {
        return false; // 不存在 pattern
      }
      p = p.children[index];
    }
    if (p.isEndingChar == false) return false; // 不能完全匹配,只是前綴
    else return true; // 找到 pattern
  }

  public class TrieNode {
    public char data;
    public TrieNode[] children = new TrieNode[26];
    public boolean isEndingChar = false;
    public TrieNode(char data) {
      this.data = data;
    }
  }
}

Trie字典樹的時間複雜度 與 缺點

插入的時間複雜度:O(N),N為所有待插入字符串的長度之和
查詢的時間複雜度:O(K),K為待查詢字符串的長度

占內存:如果用二維數組實現,每個節點就會額外需要 26*8=208 個字節
優化思路:將每個節點中的數組換成其他數據結構,比如有序數組(可以二分查找)、跳錶、散列表、紅黑樹等。

Trie變體,縮點優化:對只有一個子節點的節點,而且此節點不是一個串的結束節點,可以將此節點與子節點合併

Trie字典樹的實際應用

1.搜索引擎輸入框關鍵詞提示

因為字典樹是查找 “與前綴匹配的字符串”,又稱為前綴樹。
關鍵詞提示就是 查尋找前綴匹配的前綴合適關鍵詞,當然還有更複雜的關鍵詞排名問題,這裏不再展開。

2.自動補全功能,如:IDE編譯器自動補全,輸入法自動補全等

原理與搜索引擎類似。

3.敏感詞過濾系統

4.其它

Trie在面試與算法競賽中的例題

1.hihoCoder1014

hihoCoder1014

解題思路:Trie字典樹

首先我們把集合中的N個字符串都插入到trie中。
對於每一個查詢s我們在trie中查找s,如果查找過程中無路可走,那麼一定沒有以s為前綴的字符串。
如果最後停在一個節點p,那我們就要看看以p為根的子樹里一共有多少終結點。
終結點的數目就是答案。

但是如果我們每次都遍歷以P為根的子樹,那時間複雜度就太高了。解決的辦法是用空間換時間,我們增加一個數組intcnt[MAX_NODE]
cnt[i]記錄的是以i號節點為根的子樹中,有幾個終結點。
然後我們每次insert一個字符串的時候,順便就把沿途的節點的cnt值都+1。
這樣就不用每次遍歷以P為根的子樹,而是直接輸出cnt[P]即可。

代碼:

2.hihoCoder1107微軟面試題

hihoCoder1014

其實就是找一個節點p,滿足以p為根的子樹中的終結點不多於5個,同時以p的父節點為根的子樹中的終結點大於5個。
和上題一樣用cnt數組標記,之後dfs查找終結點的數目

3.Trie應用在整數xor異或值最大的題目

給定一個包含N個整數的集合S={A1, A2, A3, … AN}。然
後有M個詢問,每次詢問給定一個整數X,讓你找一個Ai使得Ai xor X的值最大。

首先我們知道一個整數可以用二進製表示成一個01串。比如3=(011)2, 5=(101)2, 4=(100)2……。
我們假設輸入的整數都在0~2^32-1之間,於是我們可以用一個長度是32位的01串表示一個整數。
然後對於給定的N個整數A1, A2, A3, … AN,我們把它們對應的01串都插入到一個trie中。注意這裏字符集只有0和1,所以整個trie是一棵二叉樹。

下面我們舉一個例子,為了描述方便,我們假設整數都在0~7之間,也就是可以用3位01串表示。
現在假設S={1, 2, 7},也就是說我們要在Trie中插入{001, 010, 111}:

這時假設我們要查詢x=4,也就是哪個數和4異或結果最大?4=(100)2,
我們的做法是在trie樹中,盡量與4的二進制位反着走。
比如4的第一位(最高位)是1,我們從0出發第一步就盡量沿着0走。因為我們要異或和最大,01相反才能異或值是1。
並且這一步是可以貪心的,也就是說如果有相反的邊,那麼我們一定沿着這條邊走。因為最高位異或得1的話,即便後面都是0, 10000…000也要比最高位是0,後面都是1的011111…111大。
所以我們第一步沿着標識是0的邊,移動到了1號節點;4第二位是0,所以我們沿着標識是1的邊移動到4號節點;
4的第三位是0,但是4號節點沒有標識是1的邊,所以我們也只好沿着標識是0的邊移動到5號節點。
已經到了終結點,所以5號節點對應的A2=(010)2=2就是我們要求的答案,A2 xor 4 = 6是最大的。

【精選推薦文章】

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

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

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

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

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 中有二分查找的專項練習,其中提供了三種不同的代碼模板,現在你再去看看,很容易就知道這幾個模板的實現原理了。

【精選推薦文章】

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

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

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

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

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