Google Code Prettify

2020年11月7日 星期六

Java Stream: Collections

這一篇要整理的是 import static java.util.stream.Collectors.* 配合 stream 所進行的操作,它的作用就像 reduce 一樣,但是提供了更多高可讀性的操作,看一下實作 … 先定義資料如下:
@Data
public class Product implements Serializable {
	private String name;		//商品名稱
	private String brand;		//品牌
	private String kind;		//分類
	private int price;		//價格
	private int count;		//數量
	
	public Product() { }
	
	public Product(String name, String brand, String kind, int price, int count) {
		this.name = name;
		this.brand = brand;
		this.kind = kind;
		this.price = price;
		this.count = count;
	}
}

List<Product> products = Arrays.asList(
	//精油
	new Product("橙花純精油", "怡森氏", "精油", 1179, 10),
	new Product("快樂鼠尾草純精油", "怡森氏", "精油", 879, 8),
	new Product("純天然玫瑰純精油", "怡森氏", "精油", 1569, 5),
	new Product("薰衣草純精油", "奧地利KP", "精油", 680, 12),
	new Product("真正薰衣草", "ANDZEN", "精油", 288, 25),
	//鍵盤
	new Product("G512 RGB 機械遊戲鍵盤(青軸)", "羅技", "鍵盤", 3290, 2),
	new Product("黑寡婦蜘蛛幻彩版鍵盤", "Razer", "鍵盤", 2190, 1),
	new Product("Cynosa Lite 薩諾狼蛛Lite版鍵盤", "Razer", "鍵盤", 990, 7),
	new Product("Vigor GK50 Low Profile 短軸機械式鍵盤", "MSI微星", "鍵盤", 2990, 13),
	new Product("Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)", "MSI微星", "鍵盤", 2999, 6),
	new Product("Vigor GK60 CL TC 電競鍵盤", "MSI微星", "鍵盤", 3290, 3),
	//滑鼠
	new Product("MX Master 2S 無線滑鼠", "羅技", "滑鼠", 1880, 25),
	new Product("MX Master 3 無線滑鼠", "羅技", "滑鼠", 3490, 18),
	new Product("M590 多工靜音無線滑鼠", "羅技", "滑鼠", 849, 30)
);
假設有家商店,有如上述的貨品,三類 (精油、鍵盤、滑鼠) 及多種品牌的商品,現在開始進行資料的操作:
  1. 所有商品按種類分組
  2. Map<String, List<Product>> groupByKind = products.stream()
    		.collect(groupingBy(Product::getKind));
    		
    groupByKind.forEach((k, v) -> {
    		log.info("Kind: " + k);
    		v.forEach(p -> log.info("  product: " + p.getName()));
    	});
    
    Kind: 滑鼠
      product: MX Master 2S 無線滑鼠
      product: MX Master 3 無線滑鼠
      product: M590 多工靜音無線滑鼠
    Kind: 精油
      product: 橙花純精油
      product: 快樂鼠尾草純精油
      product: 純天然玫瑰純精油
      product: 薰衣草純精油
      product: 真正薰衣草
    Kind: 鍵盤
      product: G512 RGB 機械遊戲鍵盤(青軸)
      product: 黑寡婦蜘蛛幻彩版鍵盤
      product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      product: Vigor GK50 Low Profile 短軸機械式鍵盤
      product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
      product: Vigor GK60 CL TC 電競鍵盤
    用 collect  可以展現出 functional programming 的優勢,從指令就可以看的出來,我們的需求是什麼,至於程式實際怎麼執行,不勞我們擔心,Java 會幫我們找出辦法。上述 groupingBy 指令指出以 kind (商品種類) 分組,匯整在 map 裡,後面我們將它依序輸出。
  3. 所有商品按種類分組後再依品牌分組
  4. Map<String, Map<String, List<Product>>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand)
    	));
    		
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, p2) -> {
    		log.info("  Brand: " + brand);
    		p2.forEach(p3 -> log.info("    Product: " + p3.getName()));
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技
        Product: MX Master 2S 無線滑鼠
        Product: MX Master 3 無線滑鼠
        Product: M590 多工靜音無線滑鼠
    Kind: 精油
      Brand: 怡森氏
        Product: 橙花純精油
        Product: 快樂鼠尾草純精油
        Product: 純天然玫瑰純精油
      Brand: 奧地利KP
        Product: 薰衣草純精油
        Brand: ANDZEN
        Product: 真正薰衣草
    Kind: 鍵盤
      Brand: 羅技
        Product: G512 RGB 機械遊戲鍵盤(青軸)
      Brand: Razer
        Product: 黑寡婦蜘蛛幻彩版鍵盤
        Product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      Brand: MSI微星
        Product: Vigor GK50 Low Profile 短軸機械式鍵盤
        Product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
        Product: Vigor GK60 CL TC 電競鍵盤
    上面語法,我們要多層次分組,也是很明白的"說出"我們的需求,先以種類分組再以品牌分組,就得到我們要的結果了。
  5. 計算各種類中各品牌的商品樣式數量
  6. Map<String, Map<String, Long>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand, counting())
    	));
    
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, count) -> {
    		log.info("  Brand: " + brand + " = " + count);
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技 = 3
    Kind: 精油
      Brand: 怡森氏 = 3
      Brand: 奧地利KP = 1
      Brand: ANDZEN = 1
    Kind: 鍵盤
      Brand: 羅技 = 1
      Brand: Razer = 2
      Brand: MSI微星 = 3
    利用 Collections 中的 counting() 就可以計算出數量了!
  7. 最貴的滑鼠
  8. Comparator<Product> priceComparator = Comparator.comparingInt(Product::getPrice);
    		
    Optional<Product> mouse = products.stream()
    	.filter(p -> p.getKind().equals("滑鼠"))
    	.collect(maxBy(priceComparator));
    		
    if (mouse.isPresent()) {
    	log.info(mouse.get().getName());
    }
    
    MX Master 3 無線滑鼠
    先定義 Comparator 讓 collect 的 maxBy 知道比較的方式,這裡就是比價格,在 stream 中配合 filter 找出滑鼠比一下價格,就找出最貴的滑鼠了! 這裡要注意的是,找出來的結果為 Optional, 因為有可能根本沒有滑鼠!
  9. 全部的鍵盤有幾個?
  10. int total = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(summingInt(Product::getCount));
    		
    log.info("鍵盤數量: " + total);
    
    鍵盤數量: 32
    非常簡單的,過濾出鍵盤後,用 summingInt 將數量加總。
  11. 全部的商品以種類先排序,再以分號分隔列出
  12. Comparator<Product> kindComparator = Comparator.comparing(Product::getKind);
    		
    String s = products.stream()
    	.sorted(kindComparator)
    	.map(Product::getName)
    	.collect(joining(","));
    		
    log.info(s);
    
    MX Master 2S 無線滑鼠,MX Master 3 無線滑鼠,M590 多工靜音無線滑鼠,橙花純精油,快樂鼠尾草純精油,純天然玫瑰純精油,薰衣草純精油,真正薰衣草,G512 RGB 機械遊戲鍵盤(青軸),黑寡婦蜘蛛幻彩版鍵盤,Cynosa Lite 薩諾狼蛛Lite版鍵盤,Vigor GK50 Low Profile 短軸機械式鍵盤,Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版),Vigor GK60 CL TC 電競鍵盤
    實務上資料常常要以 csv 檔產出,這時候可以用 joining,這是連接字串的 method,這裡在列出商品時,用 joining 連接並於中間加上逗點。
  13. 將精油分成怡森氏和非怡森氏兩組
  14. Map<Boolean, List<Product>> result = products.stream()
    	.filter(p -> p.getKind().equals("精油"))
    	.collect(partitioningBy(p -> p.getBrand().equals("怡森氏")));
    		
    result.forEach((k, v) -> {
    	log.info(k ? "怡森氏" : "非怡森氏");
    	v.forEach(p -> log.info("  " + p.getName()));
    });
    
    非怡森氏
      薰衣草純精油
      真正薰衣草
    怡森氏
      橙花純精油
      快樂鼠尾草純精油
      純天然玫瑰純精油
    分組除了可以用 groupingBy, 還有另一個選擇 -- partitioningBy,partitioningBy 要帶入的參數是 Predicate,只能分 true、false 兩組,但是效率比較高,如果剛好適用,partitioningBy 會是更好的選擇。
  15. 找出鍵盤中,微星與非微星品牌中最貴的
  16. Map<Boolean, Product> result = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(
    		partitioningBy(
    			p -> p.getBrand().equals("MSI微星"),
    			collectingAndThen(
    				maxBy(Comparator.comparingInt(Product::getPrice)), 
    				Optional::get)
    			)
    		);
    		
    result.forEach((k, v) -> {
    	log.info(k ? "MSI微星" : "非MSI微星");
    	log.info("  Product: " + v.getName() + ": " + v.getPrice());
    });
    
    非MSI微星
      Product: G512 RGB 機械遊戲鍵盤(青軸): 3290
    MSI微星
      Product: Vigor GK60 CL TC 電競鍵盤: 3290
    partitioningBy 還有個優點就是,在分組後,還可以使用 collectingAndThen 做進一步的處理,上面在分組後,我們在該組裡找出最貴的鍵盤。

2020年11月2日 星期一

Java Stream: operation

 本篇參見【Modern Java in Action】 2nd-edition


Stream 提供了如上表的 operation,要怎麼使用呢? 直接看例子吧~

假設一個場景,客戶進行證券交易,這裡記錄下客戶在某年交易的金額及客戶的基本資料 (姓名、年齡、居住城市),資料如下:

Customer steven = new Customer("Steven", 50, "New Taipei");
Customer shelley = new Customer("Shelley", 48, "Taichung");
Customer mark = new Customer("Mark", 53, "Taipei");
Customer kfc = new Customer("KFC", 38, "Taipei");
Customer clair = new Customer("Clair", 26, "Yunlin");
	
List<Trade> trades = Arrays.asList(
		new Trade(steven, 2018, 350000),
		new Trade(shelley, 2018, 535000),
		new Trade(mark, 2018, 1005000),
		new Trade(kfc, 2018, 810000),
		new Trade(clair, 2018, 559000),
		new Trade(steven, 2019, 67000),
		new Trade(mark, 2019, 890100),
		new Trade(clair, 2019, 90900)
	);
  1. 2018年的所有交易,按交易額排序:
  2. trades.stream()
    	.filter(t -> t.getYear() == 2018)
    	.sorted(comparing(Trade::getAmount))
    	.forEach(t -> log.info(t.toString()));
    
    得到如下結果,filter 過濾出 2018 年的交易 (Trade),以 amount 排序,從結果可以看到這些交易是那個客戶交易的。
    Trade(cTrade(cstm=Customer(name=Steven, age=50, city=New Taipei), year=2018, amount=350000) Trade(cstm=Customer(name=Shelley, age=48, city=Taichung), year=2018, amount=535000) Trade(cstm=Customer(name=Clair, age=26, city=Yunlin), year=2018, amount=559000) Trade(cstm=Customer(name=KFC, age=38, city=Taipei), year=2018, amount=810000) Trade(cstm=Customer(name=Mark, age=53, city=Taipei), year=2018, amount=1005000)
  3. 所有客戶分布於那些城市?
  4. trades.stream()
    	.map(t -> t.getCstm().getCity())
    	.distinct()
    	.forEach(t -> log.info(t.toString()));
    
    用 map 指出要保留的欄位,distinct() 表示重複的只會列出一筆。
    New Taipei
    Taichung
    Taipei
    Yunlin
  5. 列出住台北市的客戶,並以年齡排序:
  6. trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.map(t -> t.getCstm())
    	.distinct()
    	.sorted(comparing(Customer::getAge))
    	.forEach(t -> log.info(t.toString()));
    
    Customer(name=KFC, age=38, city=Taipei)
    Customer(name=Mark, age=53, city=Taipei)
  7. 住台北市的客戶的交易量總和?
  8. int total = trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.map(t -> t.getAmount())
    	.reduce(0, Integer::sum);
    log.info("total = " + total);
    
    total = 2705100
    上面的寫法會有個「封裝」的成本,int 被封裝成 Integer,或 Integer 拆裝成 int 都會需要一點點時間。為了解決這個問題,Stream 提供了 IntStream、DoubleStream、LongStream 三個原始型別的串流,那麼上面的程式可以改寫成如下:
    int total = trades.stream()
    	.filter(t -> t.getCstm().getCity().equals("Taipei"))
    	.mapToInt(Trade::getAmount)
    	.sum();
    
  9. 歷年來所有交易中,最大的一筆交易的交易量?
  10. Optional<Integer> optTotal = trades.stream()
    	.map(t -> t.getAmount())
    	.reduce(Integer::max);
    	
    if (optTotal.isPresent()) {
    	int total = optTotal.get();
    	log.info("total = " + total);
    }
    else {
    	log.info("data not found");
    }
    
    使用 reduce() 可以將資料歸納,這裡指出要資料中最大的。
    total = 1005000

【番外篇】
Arrays.asList(...) 產生的 List 會是固定大小,上面的 trades 中的元素可以更改,但是,如果要加入新元素,例如: trades.add(new Trade()); 會拋出 UnsupportedModificationException

2020年11月1日 星期日

Java Lambda: getting started

這篇會整理的是,在傳統語法下及 Lambda 語法,以排序為例進行說明:

在開始說明前,先定義一個類別如下:

@Data
public class Country implements Serializable {
	private String name;       //國名
	private String capital;    //首都
	private String area;       //位於那個洲?
	private int gdp;           //人均GDP
	private int population;    //人口總數
	
	public Country() { }
	
	public Country(String name, String capital, String area, int gdp, int population) {
		this.name = name;
		this.capital = capital;
		this.area = area;
		this.gdp = gdp;
		this.population = population;
	}
}
程式一開始會進建立一個命名為 world 的 List 如下:
List<Country> world = Arrays.asList(		
		new Country("Taiwan", "Taipei", "Asia", 24500, 23500000),
		new Country("Korea", "Seoul", "Asia", 31430, 51709098),
		new Country("Philippines", "Manila", "Asia", 3117, 107225000),
		new Country("Nederland", "Amsterdam", "Europe", 53106, 17291000),
		new Country("Canada", "Ottawa", "America", 47931, 37281000),
		new Country("Australia", "Canberra", "Oceania", 51592, 25220000)
	);
下面的例子就用人口數來排序 world 裡的國家。
  1. 定義實作 Comparator 介面的類別
  2. private class CountryComparator implements Comparator<Country> {
    	@Override
    	public int compare(Country c1, Country c2) {
    		return c1.getPopulation() - c2.getPopulation();
    	}
    }
    
    使用上述的類別規範了比較的方式,程式會如下:
    world.sort(new CountryComparator());
    
  3. 定義實作 Comparator 介面的匿名類別
  4. world.sort(
    	new Comparator<Country>() {
    		@Override
    		public int compare(Country c1, Country c2) {
    			return c1.getPopulation() - c2.getPopulation();
    		}
    	});
    
    這兩種寫法,都是 Java 7 以前的寫法,當這個用來比較的類別很短,又只會使用一次,採用匿名類別可讀性會比較高。
  5. 使用 Lambda
  6. world.sort((c1, c2) -> c1.getPopulation() - c2.getPopulation());
    
    程式怎麼會變得這麼簡單,而且清楚明白,Lambda 怎麼辦到的? Lambda 的使用有一些限制,sort 接受的 Comparator 介面只有一個 method,這是它的重要限制之一! 因為只有一個 method,Lambda expression 就看的出來是要傳入 c1、c2 兩個 Country 類別的物件進入 compare() method,compare() method 在這裡實作為 return c1.getPopulation() - c2.getPopulation()。
  7. 使用「方法引用」
  8. 這是 Lambda 的語法糖,這裡假設我們有靜態導入 java.util.Comparator.comparing (import static java.util.Comparator.comparing)。
    world.sort(comparing(Country::getPopulation));
    
    直接指明要調用 Country 裡的 getPopulation() 的值進行比較。這裡有個疑問,怎麼 Comparator 類別又多出了 comparing 這個 method 了? 而且這個 method 還不只是定義了介面,還實作了 method ! 這是 Java 8 之後才有的,interface 裡也可以實作 method,但是,因為裡面只有一個未實作的 method,仍是合乎 Lambda 的限制,Lambda 知道要使用那個 method。
    有興趣的話,再觀察一下 Comparator 的原始碼,多了不少 static method,上面只比較人口數,事實上我們可以複合比較,如下:
    world.sort(
    	comparing(Country::getArea).thenComparing(Country::getPopulation)
    );
    

2020年10月4日 星期日

Java Stream: Creation

繼上一篇「Java Stream: getting started」後,這裡整理 stream 生成的幾種方式。

  1. of
  2. Stream week = Stream.of("日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日");
    log.debug("count = " + week.count());
    
    如上,使用 of 產生 stream,上面經過 count 方法後,會 output 出 7。
  3. Arrays
  4. Arrays.stream(new String[] { "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" }, 2, 4)
    	.forEach(w -> log.debug(w));
    
    這個方法,可以從陣列中截取一段產生 stream,上面會輸出 "火曜日"、"水曜日"。
  5. generate
  6. Stream.generate(Math::random).forEach(s -> log.info("number = " + s));
    
    使用 generate 會生成無限長度的 stream,如上,會一直輸出亂數。所以,通常會配合 limit 使用。
    Stream.iterate(BigInteger.ZERO, n -> n.compareTo(BigInteger.valueOf(10)) < 0, n -> n.add(BigInteger.ONE)).forEach(i -> log.debug("integer = " + i));
    Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).limit(10).forEach(i -> log.debug("integer = " + i));
    
    上面兩種方法可以得到相同結果,都是輸出 0 ~ 9。

2020年10月3日 星期六

Java Stream: getting started

 stream 是 Java 8 新增的功能,現在 Java 已經到 15 了,但是 ... stream 似乎也沒有很普及,這裡試著整理一些基礎用法。

File resource = new ClassPathResource("data/employee.txt").getFile();
var lines = Files.readAllLines(resource.toPath(), StandardCharsets.UTF_8);
lines.stream().filter(s -> s.compareTo("1000000") > 0).forEach(s -> log.info(s));
【說明】
  1. 在 src/main/resources 目錄裡,我建了一個子目錄 data,裡面放的 employee.txt 只是一些員工編號。
  2. Files.readAllLines 可以將文字檔資料讀入,並以每一行一個 String 的方式,存在 List<String> 裡,在 Java 10 開始提供了 var 這個關鍵字,程式員可以不需要寫明型別由 java 自行判斷,將滑鼠移到 var 上,eclipse 就會顯示出正確的型別了。
  3. 所有資料存於 lines,先以 stream() 將它轉成串流,接著就可以用 stream 提供的一些方法操作。第 3 行是先透過 filter 篩選出員編大於 "1000000" 的資料,再透過 forEach 印出來。
如果資料量很大,上面的 stream() 可以改為 parallelStream(),就會以多執行緒的方式處理。一般寫 stream 的程式,步驟如下:
  1. create 一個 stream
  2. 使用 stream 的一些方法操作 stream 的內容 (可能會有多個方法一連串的操作)
  3. 在最後一個方法中,產生出結果。
感覺上 stream 很像 collection ? 以下是主要的差異:
  1. stream 沒有儲存這些資料,它只是在處理資料,以上面的例子來說,資料儲存在 List<String> 裡,轉換成 stream 並沒有另外存一份。
  2. stream 處理資料的過程不會變更原有資料,如上面,log 顯示出結果後,原本的 lines 裡的資料不會被改變。
  3. 上面的例子比較簡單,常常看到的會是串接好幾個方法,stream 會試圖延遲這些方法的執行,直到最後真的得執行時才全部的方法一次執行,如上例就是在 forEach 才會執行 filter。

2020年9月12日 星期六

安裝 SQL Server@Docker

docker hub 上有無數的 image,通常要什麼軟體,直接到上面找就行了,要安裝 SQL Server 也一樣,如下指令。 

docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=P@ssword2020" -p 1433:1433 -v D:/Docker/data:/var/opt/mssql/data -v D:/Docker/log:/var/opt/mssql/log -v D:/Docker/secrets:/var/opt/mssql/secrets -d mcr.microsoft.com/mssql/rhel/server:2019-CU1-rhel-8

【說明】
  1. MSSQL_SA_PASSWORD : sa 的密碼
  2. -p 1433:1433 : MS SQL Server 預設的 port 是 1433,這裡也將 docker 對外開放的 port 設為 1433,設成別的也沒關係,只要能對應到 docker 裡的 1433 就行了。
  3. -v D:/Docker/data:/var/opt/mssql/data : 上面指令有三個 -v,是要將 docker 的路徑與宿主機的指定路徑相對應,MS SQL Server 的這個 image 將資料存在 /var/opt/mssql/ 目錄下的 data、log、secrets,我將它對應到我的電腦的 D:/Docker 下的 data、log、secrets 目錄。
因為我有設定 volume,讓 docker 裡的 SQL Server 資料存到宿主機,就算 docker 重開,資料也不會遺失。

2020年5月3日 星期日

安裝 docker-elk

要使用 ELK,有高手已經準備好 docker 的版本,安裝設定會比較容易,這裡就整理一下 docker-elk 的安裝 (這裡假設 Ubuntu 裡已經安裝好 git、docker 及 docker-compose)。
  1. 從 github 下載
  2. git clone https://github.com/deviantony/docker-elk
    
    下載完當然就差不多如下圖所示,所有東西都在 docker-elk 目錄裡。
  3. 執行 docker-compose 啟動 ELK
  4. 在 docker-elk 目錄裡有個 docker-compose.yml,可以透過 docker-compose 操作 ELK,啟動的指令如下,預設的帳密是 elastic / changeme:
    docker-compose up -d
    
    第一次執行需要幾分鐘的時間,啟動後以 docker ps 查看,應該可以看到三個 container 在執行 (elasticsearch、logstash、kibana),並且可以看到,它們用的 port 為 9200、5000 及 5601,這些都是在 docker-compose.yml 中定義的。
  5. 測試 elasticsearch
  6. 用 curl 對 elasticsearch request 試看看:
    curl http://localhost:9200 -u elastic:changeme
    
    看到以下回應,就是成功了。
  7. 測試 Kibana
  8. 我的 Ubuntu 的 IP 是 192.168.0.155,所以我在瀏覽器上輸入 http://192.168.0.155:5601,會出現要輸入帳密的登入畫面,輸入預設的 elastic / changeme 後,會出現如下畫面: 
    確定 Kibana 也正常執行了。
  9. 測試 logstash
  10. 現在修改一下 docker-compose.yml,讓 logstash 可以用 tcp 5000 port 接收資料及用 udp 12201 port 接收資料,如下:
    接著再修改 ~/docker-elk/logstash/pipeline/logstash.conf,加入紅框的部份,表示要接收 udp 12201 port 的資料,如下: 
    按【ctrl-C】關閉 docker-elk,然後再重啟一次,再啟動後執行以下指令:
    curl -XPOST -D- 'http://localhost:5601/api/saved_objects/index-pattern' \
        -H 'Content-Type: application/json' \
        -H 'kbn-version: 7.6.2' \
        -u elastic:changeme \
        -d '{"attributes":{"title":"logstash-*","timeFieldName":"@timestamp"}}'
    
    這個指令會在 Kibana 建立一個 index pattern,然後執行以下指令,發出訊息到 ELK,看看 Kibana 會怎麼顯示。
    docker run --log-driver=syslog --log-opt syslog-address=tcp://0.0.0.0:5000 --log-opt syslog-facility=daemon alpine echo hello world tcp
    docker run --log-driver=gelf --log-opt gelf-address=udp://0.0.0.0:12201 alpine echo hello world udp
    
    然後查看 Kibana,可以看到如下的顯示:
    表示 docker 的 log 確實經由 logstash 的 tcp 5000 port 及 udp 12201 port 送到 Kibana 了。

2020年5月1日 星期五

Ubuntu 上的 PPPoE 設定

中華電信的光世代 ADSL,沒有特別申請的話,連接上去是動態 IP,如果想要固定 IP 可以到官網申請,申請核可後,電腦需以 PPPoE 的方式撥接上去取得該固定 IP,在 Ubuntu 中,方法如下:
  1. 安裝
  2. sudo apt install net-tools
    sudo apt-get install pppoeconf
    
  3. 設定
  4. sudo pppoeconf
    
    除了帳號、密碼外,每個選項都用預設值,直接按【ENTER】,帳號的部份要特別注意,中華電信給的帳號可能為 74669091,在這裡要輸入 74669091@ip.hinet.net
  5. 撥接
  6. sudo pon dsl-provider
    
  7. 中斷連線
  8. sudo poff -a
    
  9. 觀察
  10. 撥接上去後,透過 ifconfig -a 可以看到類似下面的內容,顯示有張網卡綁定了固定 IP。
    另外在 /etc/ppp 目錄下,chap-secrets 和 pap-secrets 兩個檔案裡,可以查到撥接的帳號、密碼。

2020年4月17日 星期五

K8s: Deployment

在「K8s: ReplicaSet」及「K8s: Service」兩篇,已經有說明了,當 docker hub 上有個 image,怎麼將它部署到 Kubernetes 上,並讓外部可以訪問到它。兩個步驟可以合併成一個步驟如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: host
spec:
  replicas: 3
  selector:
    matchLabels:
      app: host
  template:
    metadata:
      labels:
        app: host
    spec:
      containers:
      - name: host
        image: twleader/host
---
apiVersion: v1
kind: Service
metadata:
  name: host-svc
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 8080
    targetPort: 9080
  selector:
    app: host
注意看第 18 行,有 --- 將原本兩個 yaml 隔開合併成一個檔,透過這種方式,可以合併多個 yaml 檔,這樣部署可以快一點,但是,對 K8s 來說,這是比較低階的做法,K8s 另外提供了 Deployment 這個更高階部署 rs 的方法,yaml 可以如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: host
spec:
  replicas: 3
  template:
    metadata:
      name: host
      labels:
        app: host
    spec:
      containers:
      - image: twleader/host:latest
        name: host
指令一定要如下,不能忽略後面的 --record !! (原因後面說明)
kubectl create -f xxxx.yaml --record
使用 Deployment 比之前的做法好,這是完全交給 K8s 幫我們決定怎麼部署,除了比較容易外,還可以達到滾動式部署! (Service 還是要另外創建) 假如我們提供的服務不能中斷,換版時希望新版還沒有啟動前,舊版程式要持續提供服務,那麼使用 Deployment,改變第 14 行的版號,讓 K8s 部署新版本,K8s 會先創建新版的 pod 及 ReplicaSet,並等新版的 pod 完全啟動後,再逐步的刪除舊版的 pod。如果要下指令的話,如下:
kubectl set image deployment host host=twleader/host
名稱取的不太好,說明一下,第一個 host 是 deployment 的名稱,接著 host=twleader/host 是指,命名為 host 的 container 要由 twleader/host 這個 image 產生。在滾動更新的過程,如果要知道更新的狀況,可以下這個指令:
kubectl rollout status deployment host
部署好之後,使用 kubectl get po 可以看到舊的 pod 一個一個慢慢消失,使用 kubectl get rs 觀察,則會發現如下,
舊的 ReplicaSet 並沒有被刪除! 這方便當發現新版程式有錯,想要回到舊版時,可以更快速,要怎麼回到舊版呢? 可以使用 undo 如下:
kubectl rollout undo deployment host
事實上,K8s 每次部署都會記錄下每個版本,我們可以回滾到任何一個歷史版本,先看一下 history 指令:
kubectl rollout history deployment host
顯示如下:
之前 create deployment 時有加 --record 的話,這裡的 CHANGE-CAUSE 欄位才會有值,方便我們辨識該版本,我這邊因為都用 create 的方式,如果檔名取不同名稱可以看的出兩個版本的不同,如果是用下指令的,在 image 後面的版號就可以看出版本間的差異,要回到任何一個版本,可以下 undo 並加上 --to-revision 參數:
kubectl rollout undo deployment host --to-revision=2
要特別注意的是,K8s 的版本紀錄是保存在 ReplicaSet 裡,所以舊的 ReplicaSet 不要手動刪除,否則就無法回滾了!

2020年4月16日 星期四

K8s: ConfigMap

前一篇「K8s: 設定環境變數」是利用 create ReplicaSet 時,由 yaml 檔傳入環境變數,萬一我們的系統有許多個 ReplicaSet 要創建,每個 yaml 都要寫相同的設定,這樣就等於是 hard code 在 yaml 裡,維護相當不容易。

Kubernetes 提供了 ConfigMap 的功能,讓環境變數引用 ConfigMap 裡的值,達到集中管理的目的。怎麼建立 ConfigMap 有許多種方法,這裡舉兩個如下:
kubectl create configmap practicecnfmap --from-literal=jasypt_encryptor_password=secondKey --from-literal=spring_profiles_active=uat
這是最直覺的方式,透過命令建立一個名為 practicecnfmap 的 ConfigMap,裡面有兩個設定 jasypt_encryptor_password 及 spring_profiles_active,它們的值在這裡故意和前一篇不同,這樣才能觀察是否 ConfigMap 有產生作用。萬一 ConfigMap 裡要設定很多參數怎麼辦? 不可能在命令列這樣的輸入,可以選擇先把這些參數以 key=value 的方式存在一個目錄,檔名是 key, 內容是 value,我建立了一個 conf 的目錄,裡面有兩個檔案如下:
檔案裡是值,接著輸入如下指令, 這樣得到的結果和上面的指令是一樣的。
kubectl create configmap practicecnfmap --from-file=./conf
接著要修改 ReplicaSet 的 yaml 檔,如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: practice-rs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: practice-rs
  template:
    metadata:
      labels:
        app: practice-rs
    spec:
      containers:
      - name: practice-pod
        image: harbor.steven.idv.tw/steven/practice-pod
        envFrom:
        - configMapRef:
            name: practicecnfmap
      imagePullSecrets:
      - name: steven-harbor
第 18~20 行指出,環境變數由 practicecnfmap 這個 ConfigMap 取得。建立 ConfigMap 後,如果想要修改內容,不需刪除後重新建立,可以用以下指令編輯。
kubectl edit configmap practicecnfmap

2020年4月15日 星期三

K8s: 設定環境變數

spring.profiles.active=${spring_profiles_active}
server.servlet.context-path=/
server.port=8080

# encrypt
jasypt.encryptor.bean=encryptorBean
jasypt.encryptor.password=${jasypt_encryptor_password}
jasypt.encryptor.algorithm=PBEWithMD5AndDES
spring boot 的程式,設定多半寫在 application.properties,裡面的值基於資安,或是其它各種理由,可能不會直接寫在檔案裡,而會取自環境變數,如上的 application.properties 有兩個參數來自環境變數,在 K8s 的環境要怎麼傳入環境變數呢?

這裡簡單的寫一支 REST API,會傳回 application.properties 裡的值,看看那兩個取自環境變數的值是否正確,程式如下:
@RestController
@RequestMapping("/info")
@Slf4j
public class InfoController {
    @Autowired
    private Environment env;
    
    @Autowired
    ResourceLoader resourceLoader;
    
    @GetMapping("/app")
    public Map app() {
        Map all = new HashMap();
        Set keys = new HashSet<>();
        try {
            Resource resource = resourceLoader.getResource("classpath:application.properties");
            InputStream is = resource.getInputStream();
            Properties prop = new Properties();
            prop.load(is);
            keys = prop.keySet();
            for(Object key:keys) {
                all.put((String) key, env.getProperty((String) key));
            }
        }
        catch (IOException e) {
            log.error(e.getMessage(), e);
        }
  
        return all;
    }
} 
下面是建立 pod 的 yaml,第 9~13 行就是傳入環境變數的方式。
apiVersion: v1
kind: Pod
metadata:
  name: practice-pod
spec:
  containers:
  - name: practice-pod
    image: steven/practice-pod
    env:
    - name: spring_profiles_active
      value: "test"
    - name: jasypt_encryptor_password
      value: "mykey"
  imagePullSecrets:
  - name: steven-harbor
如果我們用 ReplicaSet 啟動兩個 pod,如下:
practice-pod 這個 pod 是用上面的 yaml 產生的,確實可以取得環境變數,但是 practice-rs-grtx7 和 practice-rs-lncdr 這兩個 pod 並無法取得環境變數,因為 rs 每次產生 pod,是從 image 產生,並沒有傳入環境變數,為了解決這個問題,建立 ReplicaSet 的 yaml 要如下 18~22 行:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: practice-rs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: practice-rs
  template:
    metadata:
      labels:
        app: practice-rs
    spec:
      containers:
      - name: practice-pod
        image: harbor.steven.idv.tw/steven/practice-pod
        env:
        - name: spring_profiles_active
          value: "test"
        - name: jasypt_encryptor_password
          value: "mykey"
      imagePullSecrets:
      - name: steven-harbor
輸入如下指令測試看看: (使用 kubectl get svc 指令,可以看到 external-ip)
curl http://172.16.3.39:9090/info/app
輸出如下結果:
{"jasypt.encryptor.algorithm":"PBEWithMD5AndDES","jasypt.encryptor.bean":"encryptorBean","server.servlet.context-path":"/","server.port":"8080","jasypt.encryptor.password":"mykey","spring.profiles.active":"test"}
可以看到,兩個環境變數都有抓到。

2020年4月14日 星期二

K8s: Ingress

繼續前面「K8s: Service」的例子,建立好服務後,服務會有對內的 cluster IP 及對外的 external IP,這樣對外服務是沒有問題了,但是,一個 Kubernetes 裡會有無數個服務,外部系統要使用這些服務有沒有類似 API Gateway 的功能呢? Kubernetes 提供的解答是 Ingress ! 它就是提供了一些基本功能的 API Gateway。
如上圖,Ingress 會根據網址導向不同的 service,以「K8s: Service」的例子,這裡寫了如下的 yaml 檔來產生 Ingress。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: steven-gw
spec:
  rules:
  - host: steven-gw.steven.idv.tw
    http:
      paths:
      - path: /host/info
        backend:
          serviceName: host-http
          servicePort: 9090
  1. 第 4 行定義了 Ingress 的名稱
  2. 第 7 行定義當 Ingress 收到這個網址的 request,會將 request 以下面的規則導向服務。
  3. 第 10 行定義,當網址開頭為 /host/info 導向 host-http 服務,port 為 9090,這是上一篇創建服務時,服務提供出來的 port。
執行上面的 yaml 後,用如下指令看一下 Ingress 綁定到那一個 IP:
kubectl get ingresses
應該會出現如下結果:
有了 IP 後,就可以在 DNS 裡設定 steven-gw.steven.idv.tw 指向該 IP,或是在 /etc/hosts 中設定,接下來就可以用如下指令測試看看。
curl http://steven-gw.steven.idv.tw/host/info
【番外篇】
上面是在 VMware PKS 上測試,PKS 上有預設安裝了 Ingress Controller,所以 ADDRESS 欄會有 IP,如果是自行安裝的 Kubernetes Cluster,因為 K8s 只提供 Ingress API 的定義,實作要再另外安裝,目前比較常見的有三種選擇:
  1. Nginx Kubernetes
  2. F5 BIG-IP Controller
  3. Ingress Kong
底下是 Ingress Kong 的安裝指令。
$ helm repo add kong https://charts.konghq.com
$ helm repo update
$ helm install kong/kong --generate-name --set ingressController.installCRDs=false

2020年4月10日 星期五

安裝 Harbor (Ubuntu)

Docker 官方的 docker registry 是 Docker Hub,以金融業來說,不可能將 docker image 放到公司外部,自建一個私有的 docker registry 就成為必要的需求,Harbor 是許多選項之一,也是目前最流行的選項之一,底下是安裝的步驟。
  • 下載 & 解壓縮
    • git hub 上下載
    • 解壓縮: tar xzvf harbor-offline-installer-v1.10.2.tgz
    • 我的 home 目錄是 /home/steven,解壓縮後檔案在 /home/steven/harbor。
    • cd harbor
  • 設定 hosts
  • 因為我希望 Harbor 的網址是 harbor.steven.idv.tw,所以在 /etc/hosts 中定義了:
    192.168.0.120 harbor.steven.idv.tw
    
  • 產生憑證
  • openssl req -newkey rsa:4096 -nodes -sha256 -keyout ca.key -x509 -days 3650 -out ca.crt
    
    openssl req -newkey rsa:4096 -nodes -sha256 -keyout harbor.steven.idv.tw -out harbor.steven.idv.tw
    
    建立一個名為 extfile.cnf 的網案,內容如下:
    subjectAltName = IP:192.168.0.120
    
    繼續產生憑證:
    openssl x509 -req -days 3650 -in harbor.steven.idv.tw -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out harbor.steven.idv.tw
    openssl req -new -x509 -text -key ca.key -out ca.cert
    
    接著將產生的三個檔案 copy 到以下目錄 (沒有這個目錄就自己建立),不只 master node,所有 worker node 的該目錄也都要有這些檔案。
    sudo cp *.crt *.key *.cert /etc/docker/certs.d/harbor.steven.idv.tw
  • 修改 harbor.yml
  • hostname: harbor.steven.idv.tw
    port: 80
    certificate: /etc/docker/certs.d/harbor.steven.idv.tw/ca.crt
    private_key: /etc/docker/certs.d/harbor.steven.idv.tw/ca.key
    
  • 安裝 Harbor
  • 執行 sudo ./install.sh --with-clair,執行過程沒有錯誤的話,打開瀏覽器,輸入 http://harbor.steven.idv.tw 應該可以看到如下畫面。(安裝前請先確定已經有安裝 docker-compose)
    預設的帳號、密碼是 admin / Harbor12345,登入後畫面如下,點選 Administrator > Users,,按【+ NEW USER】新增一個 user 命名為 steven。
    接著在「Projects」中新增一個命名為 home 的 project。
    點選 home 於「Members」將剛剛新建立的 user steven 加入,這樣 steven 才能存取 home。
  • 測試
  • 測試前先修改 /etc/docker/daemon.json,如果沒有這個檔案,自行創建,在裡面加入如下內容:
    {"insecure-registries": ["192.168.0.120"]}
    
    然後重啟 docker
    systemctl restart docker
    
    登入 docker (steven / P@ssw0rd)
    docker login harbor.steven.idv.tw
    
    因為目前 docker 裡有個 image harbor.steven.idv.tw/home/host,如下:
    所以可以用以下指令將 image 推送到 harbor 上。
    docker push harbor.steven.idv.tw/home/host
    
    檢查一下 harbor,確實已經推上去了,如下圖。
  • 啟動 & 關閉
  • sudo docker-compose up -d
    sudo docker-compose down
    

2020年1月30日 星期四

K8s: volume (PersistentVolumeClaim)

之前整理的「K8s: volume (emptyDir)」,volume 是建立在 pod 上,在 K8s 中 pod 是隨時可被消滅和重建,所以 emptyDir 只能存暫時性資料。這一篇要整理的是 PersistentVolumeClaim,volume 是建立在 cluster 上,不會隨 pod 的消滅而消失,可以用來儲存永久性資料。資料會儲存在 NFS server,怎麼建立 NFS server,可以參考「install nfs server@Ubuntu」。
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /var/nfs/www
    server: 172.31.192.65
在一般的公司,系統資源應該會由系統管理員負責並分配給程式開發人員,K8s 於是設計了系統管理員先在 K8s cluster 上建立 PersistentVolume (PV),分配了一定的資源,程式開發人員想要使用這些資源,要再建立 PersistentVolumeClaim (PVC) 由 PV 中取得,上面的 yaml 建立了 PV,有 1G 的儲存空間,下面的 yaml 建立一個 PVC,自上述的 PV 中取得 100M 的空間。

上面第 12 行的設定值為 Retain,表示就算 PVC 被刪除、回收,PV 不會被刪除且會保留 PVC 裡的資料,但是狀態由 Bound 變為 Released,要重新使用這個 PV,需手動清除上面的資料再將狀態改為 Available; 如果設定為 Delete 則當 PVC 刪除,PV 也會一併刪除。

第 13~15 行是指定 nfs 的 IP 和分享出來的路徑。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  storageClassName: manual
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
現在實際來執行一下上面兩個 yaml …
注意看上面的執行結果,建立 PV 後,PV 的 STATUS 是 Available,CLAIM 是空白,當 PVC 建立後,PV 的 STATUS 變成 Bound,CLAIM 變成 default/my-pvc,表示 my-pv 確實綁定到 my-pvc 了! 接著建一個 pod 來使用這個 PVC,在 PVC 的 yaml 檔中,不需指定要用那一個 PV,Kubernetes 會根據  PVC 宣告的內容,找到合適的 PV,因為上面的  PVC 有宣告 storageClassName,Kubernetes 會找到 storageClassName 同名的 PV,配置空間給 PVC。

要特別注意的是,PV 和 PVC 是一對一的關係,PVC 不能要求比 PV 更多的空間,但是,如上,當 PVC 要求的比 PV 擁有的少時,PV 擁有的所有資源都會給 PVC,所以,PVC 只要求 100Mi,可是 PV 有 1Gi,PVC 的 CAPACITY 會是 1Gi。
apiVersion: v1
kind: Pod
metadata:
  name: centos-pvc
spec:
  containers:
  - name: centos7
    image: centos:7
    command:
    - "bin/bash"
    - "-c"
    - "sleep 10000"
    volumeMounts:
    - name: logfolder
      mountPath: /home/steven/log
  volumes:
  - name: logfolder
    persistentVolumeClaim:
      claimName: my-pvc
my-pvc 這個 volume 在 pod 中會被 mount 在 /home/steven/log 目錄,可以使用 kubectl exec 進入 pod 中,於該目錄建立一些檔案,然後刪除 pod 再重新建立,再用 kubectl exec 進入新建立的 pod,可以在 /home/steven/log 目錄裡找到剛剛建立的檔案。最後補充說明一下上面的參數。
  • accessModes
    1. RWO (ReadWriteOnce): 僅允許單個 node 掛載讀寫。
    2. ROX (ReadOnlyMany): 允許多個 node 掛載,但 read only。
    3. RWX (ReadWriteMany): 允許多個 node 掛載,且可讀寫。

K8s: volume (gitRepo)

上一篇「K8s: volume (emptyDir)」整理的是最基礎的 emptyDir,這一篇要整理的是 gitRepo,這類型的 volume 也是一種 emptyDir,差別在於它會從指定的 git 上將相關的檔案 clone 到 volume 裡。
apiVersion: v1
kind: Pod
metadata:
  name: host-git
spec:
  containers:
  - name: host-git
    image: centos:7
    command:
    - "bin/bash"
    - "-c"
    - "sleep 10000"
    volumeMounts:
    - name: jar
      mountPath: /home/steven/host
  volumes:
  - name: jar
    gitRepo:
      repository: https://github.com/twleader/RESTAPI.git
      revision: master
      directory: .
和 emptyDir 很類似,差別在於第 18~20 行,指出了 git repository 及 revision,第 21 行則指出 clone 下來的內容要放在 volume 的當前目錄。
如上執行 yaml 後建立起 pod host-git。接下用 exec 指令進入 pod 裡面看一下 /home/steven/host 目錄裡有沒有 clone 下來的檔案。
非常順利。

2020年1月29日 星期三

K8s: volume (emptyDir)

Kubernetes 中的 volume 其實就是一個目錄,可以讓 container 間互相交換資料,或是永久保留資料。volume 提供了非常多種類型,這一篇整理的是最基礎的類型 emptyDir,這個類型的 volume 是建立在 pod 上面,建立時為空目錄,可用於儲存臨時的資料,下面會在一個 pod 上建立兩個 container 及一個 volume。
apiVersion: v1
kind: Pod
metadata:
  name: sharevol
spec:
  containers:
  - name: c1
    image: twleader/host:latest
    volumeMounts:
    - name: xchange
      mountPath: /tmp/logs
    ports:
    - containerPort: 9080
      protocol: TCP
  - name: c2
    image: centos:7
    command:
    - "bin/bash"
    - "-c"
    - "sleep 10000"
    volumeMounts:
    - name: xchange
      mountPath: /tmp/xchange
  volumes:
  - name: xchange
    emptyDir: {}
  1. 第 24~26 行,在 pod 上建立一個 volume 並命名為 xchange。
  2. 第 7~14 行,這是借用之前「部署 RESTful service 到 Kubernetes」已經建立好的一個 image twleader/host,建立一個命名為 c1 的 container。
  3. 第 9~11 行,將 volume xchange 掛載到 /tmp/logs 目錄。
  4. 第 15~23 行,建立命名為 c2 的 container,僅簡單的包含一個 centos。
  5. 第 21~23 行,將 volume xchange 掛載到 /tmp/xchange 目錄。
有了 yaml 就執行命令建立 pod 吧~
接著利用 exec 指令進入 container c1 於 volume 所在目錄建立一個檔案,如下:
再用 exec 指令進入 container c2,於 volume 所在目錄看看有沒有該檔案? 當然有!
同一個 pod 中不同的 container 透過這個方式就可以簡單的交換、共享資料了!

2020年1月28日 星期二

K8s: Service

繼續「K8s: ReplicaSet」的例子,為了讓 Kubernates 之外的程式可以存取到 pod,需要建立 service (服務),當成外界與 pod 間的橋樑。
  • 建立服務
apiVersion: v1
kind: Service
metadata:
  name: host-http
spec:
  ports:
  - name: http
    port: 9090
    targetPort: 9080
  selector:
    app: host
說明一下,這個 yaml 做了些什麼:
  1. 第 4 行定義了服務的名稱為 host-http
  2. 第 8 行指出服務開放給外界訪問的 port 是 9090
  3. 第 9 行指出 pod 的 port 是 9080
  4. 第 10~11 行指出 pod 上有 label "app=host" 的綁定到這個服務
產生服務後,以如下指令看一下服務內容。
kubectl describe svc host-http
從上面的內容可以看到服務分配到的 IP 是 10.108.180.52 (cluster IP),三個 pod 分配到的 IP 是 10.244.1.42、10.244.1.43、10.244.1.44,這些 IP 都是 Kubernetes 內部的 IP,無法由外部訪問,要測試服務是否正常工作,可以跳入一個 pod,由它對服務發出 request,如下:
kubectl exec host-2vxjs -- curl -s http://10.108.180.52:9090/host/info
注意看到,每次執行不一定會使用到同一個 pod。
  • 發現服務
  • Kubernetes 配發給服務的 IP 在服務整個生命週期不會變,但是,pod 要如何知道服務的 cluster IP ? 我們先刪除所有的 pod,rs 會自動再產生 3 個新的 pod。
    kubectl exec host-kxdtn env
    
    如上,跳入一個 pod 查看環境變數,內容如下,為什麼上面要故意刪除 pod 讓 rs 重新產生? 因為 pod 被新創立時,會根據 Kubernetes 最新狀況生成環境變數,記錄在 pod 裡面。
    PATH=/usr/local/jdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOSTNAME=host-kxdtn
    HOST_HTTP_PORT_9090_TCP=tcp://10.108.180.52:9090
    HOST_HTTP_PORT_9090_TCP_PROTO=tcp
    HOST_HTTP_PORT_9090_TCP_PORT=9090
    KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
    HOST_HTTP_SERVICE_HOST=10.108.180.52
    HOST_HTTP_SERVICE_PORT=9090
    KUBERNETES_SERVICE_PORT_HTTPS=443
    KUBERNETES_PORT=tcp://10.96.0.1:443
    KUBERNETES_PORT_443_TCP_PORT=443
    KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
    HOST_HTTP_SERVICE_PORT_HTTP=9090
    HOST_HTTP_PORT=tcp://10.108.180.52:9090
    HOST_HTTP_PORT_9090_TCP_ADDR=10.108.180.52
    KUBERNETES_SERVICE_HOST=10.96.0.1
    KUBERNETES_SERVICE_PORT=443
    KUBERNETES_PORT_443_TCP_PROTO=tcp
    JAVA_HOME=/usr/local/jdk-11
    HOME=/root
    
    注意看到第 7~8 行,顯示了服務的 cluster IP (10.108.180.52) 及開放出來的 port (9090),新創建的 pod 透過環境變數即可得知。除了這個辦法還有沒有其它辦法? 通常在網路上發現各項服務是透過 DNS,Kubernetes 是否有相同的方式? 答案是有! Kubernetes 提供了 DNS 服務,每個服務的名稱會對映到它的 IP,同時透過 FQDN,這記錄在每個 pod 的 /etc/resolv.conf 檔裡,當服務名稱找不到 IP 時,會根據這個檔的設定加上後綴,再尋找看看,現在再跳入一個 pod 來試看看。
    如上,跳入 pod host-kxdtn,並執行 bash,這樣就可以在裡面下指令,在 pod 裡面要對服務 request,最簡單的就是以服務的名稱當成網址,上面有 ping 看看,真的可以得到服務的 IP,下了 curl http://host-http:9090/host/info 也得到正確的回應,當然啦~ 服務的 port 仍需從環境變數才能得到。
  • 開放服務給客戶端
  • 上面的做法雖然讓 Kubernetes 內的 pod 可以透過服務互相訪問,但是,上面建立服務的方式,沒有將服務暴露出來給外部,K8s 外的客戶端訪問不到,現在修改 yaml 如下:
    apiVersion: v1
    kind: Service
    metadata:
      name: host-http
    spec:
      type: LoadBalancer
      ports:
      - name: http
        port: 9090
        targetPort: 9080
      selector:
        app: host
    
    加入第 6 行的設定,設定 K8s cluster 的架構為 LoadBalancer,刪除服務 host-http 並用這個 yaml 重新建立,接著查詢服務,可以得到如下結果,如果 Kubernetes 有支援負載平衡,在 EXTERNAL-IP 欄位會顯示出開放給外部的 IP,我的環境是自建的私有雲沒有支援,type 被降為 NodePort,該欄位會一直顯示 <pending>,這時候就只能使用 master node 的 IP 為 EXTERNAL-IP,port 如下面顯示的是 32015。
【番外篇】
Kubernetes 本身沒有提供負載平衡的支援,如果是使用 GCP、AWS、Azure … 就會由這些平台提供,為了解決這個問題,出現了 MetalLB 這個解決方案,依該網站提供的安裝程序安裝後,重新建立 service,EXTERNAL-IP 就會有對外 IP 了。

K8s: ReplicaSet

Kubernetes 管理 Pod 的方式是透過 ReplicationController 或 ReplicaSet,不是直接管理 Pod,ReplicaSet 是後來才有的資源,最終會完全取代 ReplicationController,也就是說 ReplicationController 在不久後的版本就會被棄用,所以這裡只整理 ReplicaSet 相關說明。

在「K8s: cluster (hostname & IP)」中,有使用到如下的指令擴容 pod host 成 3 個,並交給讓 ReplicaSet 管理。
kubectl scale rs host --replicas=3
在「部署 RESTful service 到 Kubernetes」中更有使用到如下的 yaml 直接由 image 創立 3 個 pod 交給 ReplicaSet 管理,這裡針對 yaml 做個說明。
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: host
spec:
  replicas: 3
  selector:
    matchLabels:
      app: host
  template:
    metadata:
      labels:
        app: host
    spec:
      containers:
      - name: host
        image: twleader/host
  1. 第 2 行指出要建立的資源是 ReplicaSet
  2. 第 4 行指出 ReplicaSet 的名稱為 host
  3. 第 6 行表示會產生 3 個 pod
  4. ReplicaSet 管理 pod 的方式是透過 label,第 7~9 行指出當 pod 有 app=host 的 label 時,就納入這個 rs host 管理。
  5. 第 10 行 template 以下,說明了要創建 pos,包括第 12~13 行指出每個 pod 會有 label "app=host",第 14~17 行則指出,pod 是由 image twleader/host 產生,並且 pod 的名稱為 host。
ReplicaSet 怎麼管理 pod ? 如下:

也就是說,只要有納入 rs 管理的 pod,當 pod 因為各種原因被判定為已經停止運轉,rs 會重新啟動 pod,但是如果是 pod 被刪除,則 rs 會創立一個新的 pod。下面要開始來實驗看看 …
我把 pod、rs 都刪光了,所以現在什麼都沒有 …
kubectl create -f host-rs.yaml
使用上面的 yaml 創立一個命名為 host 的 rs,及三個命名為 host-xxxxx 的 pod 交給這個 rs 管理,特別注意,這三個 pod 都有 Label "app=host"。
現在我們要刪除一個 pod,看看 rs 有什麼反應?
如上,我刪除了 pod host-dfsrc,rs 立刻又建立了一個新的 pod host-tft2x。如果我把 pod 的 label 改掉呢?
kubectl label pods host-678mg "app=hostX" --overwrite
我改掉了 po host-678mg 裡的 label 讓它變成 "app=hostX",因為 rs host 管理的是 label "app=host" 的 pod,ReplicaSet 偵測到符合的 pod 只剩兩個,所以又創建了一個 po host-c492q。
這個時候因為 pod host-678mg 沒有被任何 ReplicaSet 管理,它的存續與否不會被偵測管理。上面實驗證實了,當實際的 pod 少於預期時,ReplicaSet 會創立新的 pod,那麼,如果 pod 超過預期呢? ReplicaSet 真的會將它減少為 3 個嗎?
如上,我們觀察到了,當我把 host-678mg 的 label 改回 "app=host",ReplicaSet 偵測到符合條件的 pod 有四個,比預期的多一個,這時候就從已存在的四個中排一個刪除。上面我的刪除後查詢的太快,顯示出要被刪除的 pod 狀態為 Terminating,重新查詢後,才真的只剩 3 個。

最後,我們可以用以下指令查詢 rs 的詳細說明:
kubectl describe rs

實驗做完了,我們想刪除 rs 及所有的 pod,只要刪除 rs 即可,rs 所管理的 pod 會被一併刪除。

2020年1月27日 星期一

Docker / Kubernetes / Elastic Stack

項目日期
*
  Docker
1Installing and Configuring Docker
2017/09/04
2docker 基本指令
2018/01/03
3製作包含 java 的 docker 鏡像
2018/01/05
4使用 Dockerfile 創建 docker 鏡像
2018/01/06
5部署 RESTful service 到 docker
2019/04/05
6docker logs
2018/02/27
7安裝 Harbor (Ubuntu)
2020/04/10
8安裝 SQL Server@Docker
2020/09/12
*
  Kubernetes (K8s) / OpenShift Container Platform (OCP)
1安裝 Minikube
2019/03/23
2install Kubernetes cluster@Macbook M2
2023/10/28
3部署 RESTful service 到 Kubernetes
2020/01/08
4K8s: cluster (install)
2020/01/25
5K8s: cluster (hostname & IP)
2020/01/26
6K8s: namespace
2020/01/27
7K8s: ReplicaSet
2020/01/28
8K8s: Service
2020/01/28
9K8s: volume (emptyDir)
2020/01/29
10K8s: volume (gitRepo)
2020/01/30
11K8s: volume (PersistentVolumeClaim)
2020/01/30
12K8s: Ingress
2020/04/14
13K8s: 設定環境變數
2020/04/15
14K8s: ConfigMap
2020/04/16
15K8s: Deployment
2020/04/17
16安裝 CodeReady Container
2022/03/29
*
  Elastic Stack
1安裝 docker-elk
2020/05/03

K8s: namespace

Kubernates 提供有 namespace (命名空間) 以方便管理,這應該不難理解,現在的程式語言也多半有提供命名空間 (例如 Java 的 package),終究系統大了的話,名稱很容易重複。底下整理一些 K8s 命名空間的基本觀念。
  • 列出命名空間
  • kubectl get ns
    
    Kubernates 安裝好,預設就會有這些命名空間,在建立各項資源時,如果沒有特別指命,預設使用 default。
  • 建立命名空間
  • kubectl create namespace my-ns
    
    這是使用指令的方式,另外也可以使用 yaml,如下:
    apiVersion: v1
    kind: Namespace
    metadata:
      name: my-ns
    
    命名空間的名稱,限定只能用英數字及 dash (-)。
  • 在指定的命名空間建立資源
  • 使用如下的 yaml 重新建立一次 pod host:
    apiVersion: v1
    kind: Pod
    metadata:
      name: host
      labels:
        app: host-info
    spec:
      containers:
      - name: host
        image: twleader/host:latest
        ports:
        - containerPort: 9080
    
    指令則是在原有指令後面指定命名空間:
    kubectl create -f create-pod.yaml -n my-ns
    
    建立成功後,檢查看看,確定真的建立在命名空間 my-ns 裡了!
  • 命名空間的隔離
  • 命名空間只會隔離名稱,也就是相同名稱可以存在於不同的命名空間,但是,不同命名空間的資源,仍可互相存取到對方,不會受到任何限制!
  • 刪除資源
  • 先檢查一下目前 Kubernetes 中的 pod ... 接著我們要刪除 pod host,如下:
    kubectl delete po host
    
    因為沒有指定要刪除那一個命名空間的 pod host,預設會刪除 default 裡資源,檢查如下,default 裡的被刪除了,my-ns 的還在!
    指定命名空間,重新刪除一次,如下,正確的刪除了 my-ns 命名空間裡的 pod host。
    當 Kubernetes 收到刪除 pod 的指令,會向 pod 中的所有 container 發出 SIGTERM 的信號,並等待一段時間 (通常為 30秒) 待所有 container 正常關閉,如果時間到仍有 container 未關閉,則會直接以 SIGKILL 將它的 process 終止! 為了確保程式可以正常結束,最好可以在程式中正確的處理 SIGTERM 信號。
  • 刪除命名空間
  • kubectl delete ns my-ns
    
    這樣的指令會把命名空間及其下所有資源刪除! 如果希望只刪除命名空間下的 pod,但保留命名空間呢? 如下:
    kubectl delete po --all --namespace my-ns
    
    如果要刪除命名空間下(幾乎)所有資源呢? 指令如下:
    kubectl delete all --all --namespace my-ns 
    
    為什麼說是"幾乎"而不是全部? 因為 Pod、ReplicationController、ReplicaSet、Service 真的會被刪除,但是有少數的資源,例如 Secret 不會被刪除。
  • 切換命名空間
  • 登入 kubernetes 時,沒有特別指定,使用的是 default 這個命名空間,建立資源時,沒有指定命名空間,都會建立在 default 裡,如果我們的資源都在別的命名空間,最好是切換到那個命名空間去,這樣建立的資源就會預設是在那個命名空間,怎麼切換呢? 指令有點長,一般會先建立個 alias,如下:
    alias kcd='kubectl config set-context $(kubectl config current-context) --namespace '
    
    之後要切換命名空間,例如,有個命名空間為 home,要切換到 home,就這麼下指令:
    kcd home
    

2020年1月26日 星期日

K8s: cluster (hostname & IP)

接續上一篇「K8s: cluster (install)」,在安裝了 Kubernetes cluster 之後,現在要來觀察裡面的配置。
  • 部署一個 demo 程式
  • @RestController
    @RequestMapping("/host")
    public class HostController {
        @GetMapping("/info")
        public String info() {
            try {
                InetAddress ip = InetAddress.getLocalHost();
                return ip.getHostName() + " (" + ip.getHostAddress() + ")\n";
            } catch (UnknownHostException e) {
            }
            
            return "Unknown\n";
        }
    }
    
    這個 REST API 非常簡單,每次被呼叫就會傳回 hostname 和 ip,這樣我們就可以觀察安裝好 Kubernetes cluster 之後,倒底 Kubernetes 進行了怎麼樣的配置。部署的細節可以參考「部署 RESTful service 到 Kubernetes」,這裡假設已經部署了一個命名為 host 的 pod,並且服務名稱為 host-http。為了觀察,這裡先執行以下指令,讓 Kubernetes 把 Pod 增加為 3 個。
    kubectl scale rs host --replicas=3
    
  • IP & hostname
  • kubectl get pod
    
    要怎麼知道真的有 3 個 Pod ? 如上指令,可以看到紅框裡新產生的三個 Pod,並且知道這 3 個 Pod 的 hostname 分別為 host-24kw7、host-g7xdt、host-prlrl。那麼這 3 個 Pod 的 IP 呢? 如下 …
    kubectl describe svc host-http
    
    這個指令會把服務的詳細內容列出來,從這裡顯示的資訊可以了解,這 3 個 Pod 的 IP 分別為 10.244.1.14、 10.244.1.16、10.244.1.17,並且知道服務對內的 IP 為 10.106.92.190 (cluster ip)。那麼對外的 IP 呢? 就是機器本身的 IP,master node 的 IP 是 192.168.0.110,來測試一下 …
    curl http://192.168.0.110:32738/host/info
    
    真的顯示出其中一個 Pod 的 hostname 和 IP。上面的方法可以得到 hostname 和 IP,但是倒底那一個 hostname 對到那個 IP 卻不知道,如下的指令可以得到資訊。
    kubectl get pods -o wide
    
    根據上面的資訊,可以得知整個架構如下:
    為什麼 pod 的 port 是 9080? 因為我在程式的 application.properties 定義了如下的參數:
    server.port=9080
    server.servlet.context-path=/
    
    【番外篇1】
    pod 隨時可以被消滅,每次建立時 IP 並不一定會一樣,pod 間要相互訪問,可以透過 cluster IP,例如:
    kubectl exec host-g7xdt -- curl -s http://10.106.92.190:9080/host/info
    
    上述的指令是直接進到指定的 pod host-g7xdt,下 curl 指令。
    【番外篇2】
    不通過服務 (service) 的情況下與特定的 pod 通訊也是有辦法,Kubernetes 提供的轉發的機制,使用 kubectl port-forward,方法如下:
    kubectl port-forward host-g7xdt 10080:9080
    
    我把外部的 port 10080 對映到 host-g7xdt 的 port 9080,執行後會如下:
    接著開啟另一個 terminal,執行 curl 測看看,如下,仍可得到正確的回應。