Ada Hsu 的胡思亂想

2019年4月28日

啟用 TensorFlow 的 Intel CPU 擴充指令集

昨天老師在課堂示範了使用 Keras 搭配 TensorFlow 做底層的範例程式,以 minst 資料集 的 60000 個數字圖檔進行圖像辦識訓練,這個範例程式透過 Keras 建立了 2 個隱藏層,每次訓練 100 筆資料並持續測試 20 輪,Keras 的 Dense 資訊如下:
Layer (type) Output Shape Param #
dense_1 (Dense) (None, 689) 540865
dense_2 (Dense) (None, 689) 475410
dense_3 (Dense) (None, 689) 475410
dense_4 (Dense) (None, 10) 6900
一開始是在 Jupyter 的 console 中看到下面這段訊息,它顯示了 TensorFlow 沒有真正使用CPU 的擴充指令集,而透過 pip search 發現有個可疑的套件 intel-tensorflow,而我才剛剛在 MacOSX 上透過 PlaidML 啟用了 Keras 對 AMD Radeon 560X 的支援,不妨來看看這幾種組合對 Keras 訓練的影響。
I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA

先說結論

不一定需要有多強大的 GPU 硬體支出,大部份電腦只要額外安裝 intel-tensorflow 套件就能有效減少訓練時間。

補充說明:

  1. 請同學在 Windows 上測試,認為似乎裝不裝 intel-tensorflow 都一樣慢
  2. 個人測試 CPU Extension 全開的 tensorflow 比 AMD Radeon 560X GPU 版本還快
  3. 但 Evaluating PlaidML and GPU Support for Deep Learning on a Windows 10 Notebook 這篇文章看起來則是 GPU 有絕對優勢,但它的範例程式我跑不了... 會出以下訊息
    ValueError: `steps_per_epoch=None` is only valid for a generator based on the `keras.utils.Sequence` class. Please specify `steps_per_epoch` or use the `keras.utils.Sequence` class.

補充說明 20190806:

  1. 在 tensorflow 1.14.0 之後似乎 Intel 優化版已整併到官方版本內,pip 內的版本停在 1.13.1 中,1.14 啟動時的訊息也只剩 AVX2 FMA 未啟動
  2. 另外發現在 Mac 上使用 PlaidML 當 Keras 後端時,使用 Apple 自家的 Metal API 效率會比 OpenCL 好約 1/3(94 秒 --> 68 秒)
  3. CPU Extension 全開的客制版目前還沒有可和 tensorflow 1.14.0 版搭配的 MacOS 平台版本,大概只能自行建置

各種 TenforFlow 版本安裝

通用標準版
沒有經過特別設定的的話應該都是安裝這個版本的 TensorFlow,也就是經由以下指令安裝的版本。在執行過程中會在 Console 中偵測 CPU 能提供什麼擴充指令集然後顯示出來。
pip install tensorflow
TensorFlow 對 CPU 擴充指令集的提示像這樣:
I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
Intel 優化版
從名字可以知道是 Intel 調教過的版本,主要是啟用 Intel CPU 的 SSE4.1 SSE4.2 AVX 這 3 個擴充指令集。套件資訊中雖然沒有寫相依性套件資料,但個人覺得應該有依賴於 tensorflow 官方套件。
$ pip show intel-tensorflow
Name: intel-tensorflow
Version: 0.0.1
Summary: Intel Optimized Tensorflow with MKL
Home-page: https://github.com/IntelAI
Author: Intel Tensorflow optimization team
Author-email: [email protected]
License: UNKNOWN
Location: /usr/local/anaconda3/envs/ai/lib/python3.6/site-packages
Requires: pip
Required-by:
安裝這個套件後,TensorFlow 對 CPU 擴充指令集的提示變成這樣:
I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
客制版
就算是 Intel 優化版的 TenforFlow 也沒有完整啟用 CPU 擴充指令集,於是就有強者在 GitHub 上預先編譯 TensorFlow 套件並加入完整擴充指令集設定,如 tensorflow-buildtensorflow-windows-wheel
套件安裝方式只要使用以下 pip 指令即可,但找到對的套件包很麻煩。
pip install --ignore-installed --upgrade "下載的 .whl 檔完整路徑"

各種組態下的訓練狀況

此處針對文章開頭的情境分別進行訓練,每次訓練前都會關閉 Jupyter (因為要安裝套件)並直接對 Jupyter Kernel 執行 Restart & Run All 的結果。
硬體環境為:MacBook Pro (15-inch, 2018), 2.6 GHz Intel Core i7, Radeon Pro 560X 4 GB 獨立顯示卡。
Backend 類型 花費時間(秒) CPU 使用率 風扇運轉狀態
tensorflow (1.13.1) 211.579 1100% ↑ 全速
intel-tensorflow (1.13.1) 113.840 800% ↓ 微速
客製版 1.13.1 97.193 700% ↑ 微速
PlaidML w/ AMD Radeon Pro 560X 91.528 250% ↓ 全速
測試用的 Code 可參考 keras_MNIST.ipynb,要自行驗證的話記得把 backend 改回 tensorflow。

2019年1月31日

使用 nightwatch.js 進行 E2E 測試

這兩天來努力使用 nightwatch.js 對網站進行驗證,一些小心得先記錄一下好了。以下內容不一定是正規解法,有比較好的方式歡迎留言討論,謝謝。

nightwatch.js 目前還不支援 Data Driven Testing

整個場景是要針對已知的數筆資料(可能會增減)對網站系統進行登入及查驗的動作,這些資料被塞在一個 .txt 檔內,理想狀況是 nightwatch.js 打開該檔後依序跑指定的 test case。
但是它沒有迴圈的概念,所以永遠只跑第一筆記錄...
Google 後有人說可以用它的 mocha runner,但是... 看不懂啊... XD
沒關係,macOS 環境下有 shell,在外面跑迴圈把資料餵食給 nightwatch.js 總行了吧?

奇怪的命令列參數傳遞

待測試的資料是數字型的,直接丟給 nightwatch.js 竟然報錯...
node_modules/nightwatch/bin/nightwatch  tests/crawler.js 1234345678
   The "path" argument must be of type string. Received type number
       at validateString (internal/validators.js:125:11)
       at Object.resolve (path.js:1080:7)
最後傳參數的方式變成以下這種很奇怪的格式:
node_modules/nightwatch/bin/nightwatch  tests/crawler.js -ABCDEFG -12345678
在 nightwatch.js 內取得參數的方式倒是不難,就是濾掉 - 號以及檔案內可能參雜的 \r 或 \n 而以。
const args = process.argv.slice(3);
const param1 = args[0].replace( /^-/, '').replace( /\r/, '').replace( /\n/, '');
const param2 = args[1].replace( /^-/, '' ).replace( /\r/, '').replace( /\n/, '');

點選某一個 radiobutton

本來應該使用 .click() api 去點選頁面上的元件,但它馬的不會動啊... 最後透過 .execute() 搭配 document.querySelector() 方式總算點到了... = =
browser
    .execute(function(){
        document.querySelector( '#A > span > input[type="radio"]' ).click();
        document.querySelector( '#B > div.section_wrap > form > ul > li.ht > div.ipt_wrap_2 > label:nth-child(1) > span > input[type="radio"]' ).click();
    })

checkbox 還要檢查是不是可見元素

一樣還是透過 .execute() 及 document.querySelector() 取得 checkbox 本身,然後檢查它的 offsetParent 屬性是不是 null,若是 null 表示使用者看不到該 checkbox ,可以不用理它。
另外,別忘了檢查 checked 屬性,亂點可是會捅漏子的...
let box = document.querySelector( '#A input[type="checkbox"]' );
if( box.offsetParent != null && box.checked == false ) {
    box.click();
}

關於 elements() API

如果單純在 elements() 的 callback() 檢視它的傳回值時,會看到一組類似這樣的東西:

{ sessionId: '31bf7adb53f4f9a93a0c4404e41d303f',
  status: 0,
  value:
   [ { ELEMENT: '0.7637505561737095-2' },
     { ELEMENT: '0.7637505561737095-3' },
     { ELEMENT: '0.7637505561737095-4' } ] }
這似乎是 Selenium 內部用來管理 element 用的代號,可以透過 .elementIdXXXXXX() 之類的 API 取得真正的 element 的指定屬性(對,是那個指定屬性放在 .value 屬性內,不是傳回該 element ),所以可以這樣子捉出頁面上的資訊:
browser
    .elements( 'css selector', 'div.plan_items', function(result){
         result.value.forEach(element => {
            browser.elementIdAttribute(element.ELEMENT, 'id', function(result){
                console.log( result.value );
                planList.push( result.value.replace( /^plan_items_/, '' ) );
                });
            });
        })

結論

這些心得微小的不足為外人道,但它馬的花了我 2 天啊....
補個幹!

2018年5月10日

SHELL 下輸出貼齊的訊息


function getPosition {

    echo -ne "\033[6n"            # ask the terminal for the position
    read -s -d\[ garbage          # discard the first part of the response
    read -s -d R pos              # store the position in bash variable 'foo'
    Y=$(echo "$pos" | cut -d\; -f 1)
    X=$(echo "$pos" | cut -d\; -f 2)
}


## 以指定顏色在 TERMINAL 右側顯示指定內容
function showStatus {

    WIDTH=$(tput cols)

    COLOR=$1
    MSG=$2

    LENGTH=$(echo ${MSG} | wc -c)

    # 取得目前座標 X: col, Y: row
    getPosition

    NEW_POS=$(expr ${WIDTH} - ${LENGTH})
    REPEATE=$(expr ${NEW_POS} - ${X})
    printf '.%.0s' $(seq 1 ${REPEATE})
    echo -e "${COLOR}${MSG}${COLOR_RESET}"

}

COLOR_HIGHWHITE='\033[1;37m'

echo
echo -n "This is a Book"
showStatus "${COLOR_HIGHWHITE}" "[YES]"

以下是執行範例:

2017年11月10日

在 macOS 上設定炫酷的 終端機 操作環境 - 使用 oh-my-zsh w/ Powerlevel9k

2020/01/06 補充:呃,我把它升級 (?) 到 Powerlevel10k 了,Powerlevel10k 建議的字型也不錯看,只是大部份的 icon 都變小了... :(
2018/10/25 補充:更多的客製化:Powerlevel9k: personalise your terminal prompt for any programming language
2018/08/15 補充:如果喜歡 Fira Code 字體的話,可以套用 font-firacode-nerd-font
這兩天把個人在使用的終端機環境改造了一番,很能自嗨,所以留個記錄... XD
  1. 先裝 Homebrew
  2. 再安裝 caskroom
    
    brew update
    brew install cask
    
    
  3. 參考這裡完成 zsh / oh-my-zsh 的安裝
  4. 安裝 Powerlevel9k
  5. 安裝相關字型
    • 基本上就是 Powerline / Awesome / Nerd 三類字型選一個,個人建議 Nerd 類字型即可,因為它的 icon 最完整
    • 以下指令可以啟用 caskroom 的字型庫管理
      
      brew tab caskroom/fonts
      brew update
      
      
    • nerd 相關的字型很多,可以挑一個自己喜歡的來裝,這裡安裝 Source Code Pro 版本
      
      brew search nerd
      
      
  6. 編輯 ~/.zshrc ,設定 Powerlevel9k 運作模式
    
    # 找到 ZSH_THEME 字串位置,改成如下:
    
    POWERLEVEL9K_MODE=nerdfont-complete
    ZSH_THEME="powerlevel9k/powerlevel9k" 
    
    
  7. 參考作者的說明別人的設定,組合出自己最酷炫的終端機界面,重點可先放在以下幾項測試:
    • POWERLEVEL9K_LEFT_PROMPT_ELEMENTS
    • POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS
    • POWERLEVEL9K_PROMPT_ADD_NEWLINE
    • POWERLEVEL9K_PROMPT_ON_NEWLINE
  8. 個人的設定如下,加在 ~/.zshrc 檔案末端:
    
    POWERLEVEL9K_SHORTEN_DIR_LENGTH=2
    
    POWERLEVEL9K_TIME_BACKGROUND="white"
    POWERLEVEL9K_TIME_FOREGROUND="black"
    # POWERLEVEL9K_TIME_FORMAT="%D{%H:%M:%S}"
    
    POWERLEVEL9K_VCS_GIT_ICON='\uf1d3'
    POWERLEVEL9K_VCS_GIT_GITHUB_ICON='\uf09b'
    # POWERLEVEL9K_VCS_CLEAN_FOREGROUND='blue'
    # POWERLEVEL9K_VCS_CLEAN_BACKGROUND='black'
    # POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND='red'
    # POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND='black'
    # POWERLEVEL9K_VCS_MODIFIED_FOREGROUND='black'
    # POWERLEVEL9K_VCS_MODIFIED_BACKGROUND='black'
    
    POWERLEVEL9K_STATUS_VERBOSE=true
    POWERLEVEL9K_STATUS_OK_IN_NON_VERBOSE=false
    
    POWERLEVEL9K_VPN_ICON='\uf023'
    
    POWERLEVEL9K_PROMPT_ON_NEWLINE=true
    POWERLEVEL9K_PROMPT_ADD_NEWLINE=true
    POWERLEVEL9K_FOLDER_ICON='\uf114'
    
    # Bateria
    POWERLEVEL9K_BATTERY_CHARGING='yellow'
    POWERLEVEL9K_BATTERY_CHARGED='blue'
    POWERLEVEL9K_BATTERY_LOW_THRESHOLD='20'
    POWERLEVEL9K_BATTERY_LOW_COLOR='red'
    POWERLEVEL9K_BATTERY_CHARGED_BACKGROUND='46'
    POWERLEVEL9K_BATTERY_CHARGED_FOREGROUND='235'
    POWERLEVEL9K_BATTERY_CHARGING_BACKGROUND='178'
    POWERLEVEL9K_BATTERY_CHARGING_FOREGROUND='235'
    POWERLEVEL9K_BATTERY_LOW_BACKGROUND='88'
    POWERLEVEL9K_BATTERY_LOW_FOREGROUND='235'
    POWERLEVEL9K_BATTERY_CHARGED_ICON='\UF240'
    POWERLEVEL9K_BATTERY_LOW_ICON='\UF240'
    POWERLEVEL9K_BATTERY_CHARGING_ICON='\UF1E6'
    POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND='white'
    POWERLEVEL9K_BATTERY_DISCONNECTED_BACKGROUND='red'
    POWERLEVEL9K_BATTERY_ICON='\UF1E6'
    
    # Anaconda
    POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(anaconda battery)
    POWERLEVEL9K_ANACONDA_RIGHT_DELIMITER='>'
    POWERLEVEL9K_ANACONDA_LEFT_DELIMITER='<'
    POWERLEVEL9K_ANACONDA_BACKGROUND='24'
    POWERLEVEL9K_ANACONDA_FOREGROUND='111'
    # POWERLEVEL9K_ANACONDA_ICON=''
    
    POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(os_icon context dir dir_writable vcs )
    POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status root_indicator background_jobs command_execution_time time battery anaconda)
    
    
  9. 記得在終端機中選定使用的字型為剛剛安裝的 Nerd 字型,不然只會看到亂碼 1/2 ... (這哏好冷)
  10. 效果如下,謝謝收看!


2017年7月28日

從 Google Container Engine 連線到 Google SQL for MySQL / 以 Java 為例

看了一下發文記錄,這應該是此格在停業六年後重新生出來的文章,就... 留個記錄吧!

這篇內容是以個人已理解的資訊為基礎所撰寫,特色是知其然而不知其所以然,所以不保證理解上的正確性,請參考官方文件進行相互確認,謝謝。

源起

我是在 2016 年時開始接觸容器應用的,不過實務上一直沒啥機會應用。後來在前公司的 2017 年計劃中原本有一項重點是預訂將營業相關的應用系統自單機移轉到雲端上(因為服務是面向全球),但公司停業了(所以我失業了),這項計劃也就不了了之。

不過工作好像不是那麼好找,於是趁著找工作時的空檔,試著把這個計劃驗證一遍。

營業系統的核心架構是 APIs + MySQL,原本是在 Google 上開設兩台 VM 各司其職,實務上它並不具有 Non Stopping 的服務等級要求,也就是說若 API Server 或 MySQL 資料庫崩潰了,在管理人員進行修正前服務都是停擺的。

這次選擇的是雲端平台是 Google Cloud Platform,理由只是因為它直接提供了 Cloud SQL for MySQL,在資料量不多的情況下相信可以直接透過 mysqldump 無痛轉移資料庫內容。

示範平台

本案範例是在 macOS Sierra 上進行驗證,系統上包含了 Docker for Mac 17.06.0-ce、Java SDK 1.8.0_102、以及 MySQL Client for 5.7.17。Java 打包工具則為 Apache Maven 3.5.0,但應該 3.x 以上版本都能適用。

範例中使用的操作指令大致上都可以在各平台下操作,必要時會針對 Windows 平台部份進行猜測說明。

試用 Google Cloud Platform 

Google 針對 Google Cloud Platform (以下稱 GCP)提供了 12 個月 300 美金的試用額度,這對於懶人測試員有很大的誘因,如果你不想在 2 ~ 3 星期內完成一大堆的測試,那麼  GCP 可以讓你安心的將測試項目安排在 52 星期內慢慢進行... XD

啟用的方式很簡單:
  1. 準備好一個 Google Account
  2. 準備好一張信用卡,Google 聲稱未取得使用者同意前不會扣款(有加入 Android Pay 的話應該會直接使用相關的設定)
  3. 登入  Google Cloud Platform Console 後,按下 TRY IT FREE申請免費試用按鈕
  4. 依頁面指示填寫資料,這部份我忘了要填什麼了... XD
  5. 然後就正式啟用了

建立 GCP 專案

GCP 透過專案的概念協助管理人員於單一介面內管理與專案有關的各種 Google 服務,所以第一個就是建立 GCP 專案,上圖中紅框部份都能進入建立 GCP 專案的流程。

建立專案所需要填寫的基本資料不多,就是給定一個專案名稱而以,然後 Cloud Console 會在專案名稱不衝突的情形下會將這個專案名稱設定為專案 ID,這個專案 ID 在之後會常用到。


決定好專案名稱及專案 ID 後即可建立一個新的 GCP 專案,Console 需要一點時間進行資源配置,當配置完成後即可在專案的資訊主頁上方確認當前的操作所影響的專案名稱,而專案資訊這張卡片也會顯示目前使用中的專案名稱及專案 ID。



建立專案後會看到一個入門指南的卡片,可以花點時間看看。

建立 Cloud SQL 資料庫

回到前面所提到過的『營業系統的核心架構是 APIs + MySQL』這件事,大部份開發人員所想到的第一個問題應該就是如何建立一個雲端資料庫了!

在 Cloud SQL 上建立一個 MySQL 資料庫很簡單,除了透過 Google Cloud SDK 以指令方式建立外,還可以透過 Cloud Console UI 建立。

首先在 Cloud Console 資訊主頁中拉出左側的產品與服務選單、點選 SQL 服務然後進入 Cloud SQL 管理界面。

建立 MySQL 執行個體

目前並無任何執行中的 SQL 執行個體,所以必須點選建立執行個體鈕進行資料庫的建置動作,這邊可以選擇使用 MySQL 或 PostgreSQL(但是 Beta 中)資料庫,本案將使用 MySQL。


選擇使用的資料庫種類,此處選 MySQL


依照建議選擇 MySQL 第二代的執行個體類型

填寫 MySQL 基本參數


  1. 執行個體 ID:日後要用什麼名字來識別這個資料庫
  2. 根密碼:即 MySQL root 使用者的密碼
  3. 位置:資料庫要放在哪個區域,其中 asia-east1 指的是台灣,此處建議連分區也一併指定,後續建立其他環境時可以集中在同一機房內
    ※詳細的區域資訊可以參考 Cloud Locations

調整進階參數

點選顯示進階選項後有一些額外設定,設定後請記得按下關閉鈕以確保相關的變更被記錄下來。

  1. 選擇資料庫版本:指定 MySQL 資料庫的版本,預設是 5.7
  2. 設定機器類型和儲存空間:指定 MySQL 運作環境的資源,請依用途妥善選擇對應的資源環境
  3. 啟用自動備份與高可用性:決定是否進行自動備份,此處全部勾選以作為日後驗證備份機制之參考;建立容錯移轉備用資源指的是會在另一個機房建立一個資料庫備份,所有對資料庫的操作都會複製一份到備用資源這個執行個體來
  4. 授權網路:開放哪些  IP 可透過 MySQL Client 連接資料庫
  5. 新增 Cloud SQL 標記:等同於 my.cnf 中的設定,對於效能的最佳化參數都在此調整;lower_case_table_names 這是為了讓表格名稱可以不分大小寫(事實上就是強制小寫)之用
  6. 設定維護時間:用途不明,不予解釋
本案最後的參數設定如下,確認沒有問題後按下建立鈕開始建置  MySQL 資料庫。

建置 MySQL 資料庫中的畫面,它需要一點時間,可以先泡個咖啡再回來。

完成後的資訊主頁會像這樣,其中可以注意會有兩個對外 IP 且分佈於不同機房。


點選各 Instance ID 後即可管理該執行個體,除了檢視運作狀態外還包含修改配置、匯入、匯出、開關服務、複製、甚至刪除資料庫等操作。

測試是否可以透過 MySQL Client 分別連上這兩個執行個體。
  1. 主資料庫
  2. 容錯資料庫

建立空白資料庫

為了後續測試所需,此時先在主資料庫 demo 上建立一個空白資料庫,名稱為 springbootdemo,這個資料庫會自動複製到容錯資料庫 demo-failover 去。
CREATE DATABASE springbootdemo
  DEFAULT CHARACTER SET utf8
  DEFAULT COLLATE utf8_general_ci;

IP 參照設定

為了便於操作,可將 Cloud SQL for MySQL 的兩個  IP 寫入 /etc/hosts (Widnows 平台請參考 手動設定網址與 IP 對應的 hosts 檔教學,適用 Windows、Mac OS X 與 Linux 系統 一文說明)內,此處設定 mysql.gcp 對應到主資料庫,failover.gcp 對應到容錯資料庫

Spring Boot 示範程式

再來是開發一個可以存取 Cloud SQL for MySQL 的應用程序,這裡選擇使用 Spring Boot 開發框架。

選擇 Spring Boot 的理由主要是因為它可以整合 Tomcat 這類型的 Application Server,當我們需要將服務置入 Docker Container 中執行時就可以不必額外處理 Tomcat 在各種不同 Linux 散佈套件的差異。

複製範例程式

這個範例程式的功能是模擬兩個 API,分別是顯示被呼叫的主機名稱中止應用程序(模擬程序崩潰)。請自 Github 上複製 SpringBootDemo 這個專案,關於 Spring Boot 的設定細節請自行參考網路文章,此處不特別說明:

以下命令列指令可以將 SpringBootDemo 專案複製到本地工作:
git clone https://github.com/AdaHsu/SpringBootDemo.git
整個示範程式的目錄結構如下圖,需要自行調整的參數設定都在 Maven 的設定檔 pom.xml 中:

修改 Maven 設定檔

在 pom.xml 中要修改的地方有 3 個,主要和資料庫存取有關:
  1. local profile 下的 JDBC 連線資訊:確認已設定好 mysql.gcp 的 IP 參照,或直接將 mysql.gcp 替換為 Cloud SQL for MySQL 提供的對外 IP

  2. JDBC 的使用者帳密設定:一般環境下應該要建立應用程式專屬帳號

測試程式運作狀態

完成修改後即可進行功能測試,測試前請先確認 Cloud SQL for MySQL 執行個體已啟用,然後下達以下指令打包程式,-P local 是指定產生本地測試用的版本,若為 -P gcp 則會產生 GCP 上適用的版本,可以從打包出來的檔名確認對應的執行環境。
mvn -P local clean package
完成打包後會產生 target 目錄,此處已設定打包後的 jar 檔檔名會依指定的 Profile 不同而變化。


為確保程式運作正常,建議先檢查資料庫連線參數是否正確設定,此參數應與 Maven 設定檔中設定的內容一致。
head target/classes/application.properties


如果是 Windows 平台,那麼指令應該要改成如下:
type target/classes/application.properties

預先以 MySQL Client 連上資料庫,確認資料庫內未建立任何表格。


啟動 Spring Boot 應用程式,若 JPA 能連接資料庫的話就不會有錯誤訊息,否則就會崩潰結束。
java -jar target/SpringBootDemo-local.jar


檢視資料庫,確認 access_logs 表格已被建立出來。

驗證 API

第一個 API 的用途是將呼叫記錄寫入資料庫內,並且傳回提供服務的主機名稱,在部署到 Google Container Engine 時將用於識別是哪一個容器 / Pod 提供服務的。
curl http://localhost:8080


第二個  API 的用途是中斷 Spring Boot 應用系統的執行,目的是模擬系統異常導致服務終止的情形。
curl http://localhost:8080/crash


呼叫結束後可以確認到 Spring Boot 應用程式被終止。


檢視資料庫,確認兩次呼叫記錄已被保存。


確認容錯資料庫內容也已同步更新。
echo "select * from access_logs;" | mysql -u root -h failover.gcp -p springbootdemo

打包 Docker 映像檔

從 Github 上下 clone 下來的檔案庫中包含兩個協助打包的 Dockerfile ,分別對應到打包成 local 及 gcp 兩個環境。

打包本地測試用映像檔

以下指令可以打包一個本地進行測試用的  docker image,MYSQL_IP 是記錄 /etc/hosts 中 demo.gcp 所指向 Cloud SQL for MySQL 的 Public IP,Windows 平台使用者請直接將 ${MYSQL_IP} 替換成 IP  位址即可:
MYSQL_IP=$(grep mysql.gcp /etc/hosts | cut -d\  -f 1)
docker build -t spring-boot-demo:1 \
  --add-host=mysql.gcp:${MYSQL_IP} \
  -f Dockerfile.local .


若是已將 pom.xml 中的 JDBC 連線字串替換成 IP 位址的話,那麼打包指令中的 --add-host 這段參數可以拿掉。

若是 Windows 平台可以直接給定 mysql.gcp 所對應的 IP,以下指令請記得替換 <mysql public ip> 為實際 IP:
docker build -t spring-boot-demo:1 --add-host=mysql.gcp:<mysql public ip> -f Dockerfile.local .

驗證映像檔 

打包完畢後可以啟動這個 Container 映像檔確認是否正常運作。
docker run --rm -p 8080:8080 spring-boot-demo:1
檢視 API 是否可被呼叫?

打包 GCP 線上版本

GCP 下載 Docker Images 的來源有二:第一種是上傳到 Docker Hub,另一種方式則是上傳到專案所在的 Cloud Storage 中。上傳到 Cloud Storage 時需指定 Docker 映像檔的 Tag,格式為 gcr.io/<專案 ID>/<映像檔名>:<標籤>,其中 <專案 ID> 請參考專案資訊主頁中的專案資訊卡片。


以下指令可以打包一個在 GCP 上運行的  Docker 映像檔,為了避免包裝映像檔失敗,此處會重新打包 Spring Boot 範例程式:
mvn -P gcp clean package
docker build -t gcr.io/springboot-demo/spring-boot-demo:1 -f Dockerfile.gcp .


稍候會將這個映像檔上傳到 Google Cloud Storage 中供 Container Engine 叫用。

建立 Google 容器叢集

Google Container Engine (GCE,有時會看到 GKE 指的是同一個服務) 是 Google 提供的容器服務,在使用容器服務之前需要建立一個用於部署容器的容器叢集

拉出左側產品與服務選單,點選 Container Engine 功能項後即可進入 Container Engine 資訊主頁,第一次進入這個主頁時可能需要一點時間啟用 Container Engine 服務。


有時候可能會發生沒有 Compute Engine API 權限而無法啟用 Container Engine 服務的問題,通常只要重新進入 Container Engine 資訊主頁即可改善。

建立容器叢集

在 Container Engine 資訊主頁中可以透過建立容器叢集按鈕建立新的叢集。

容器叢集基本上是透過 Compute Engine 裡頭的 VM 組建而成,它需要決定這個叢集的運作能量(財力決定能力?)。
  1. 設定叢集參數(一)
    • 名稱:本叢集的名稱
    • 說明:若有多個叢集共存的情形下,可以在此填寫一些說明內容
    • 區域:要將叢集放在哪個機房,此處放在先前建立的 Cloud SQL for MySQL 相同的機房內
    • 叢集版本:這個應該不用動
    • 機器類型:視需要自行調整,老話一句:財力決定規格、規格決定能力
  2. 設定叢集參數(二)
    • 節點映像檔:此案單純將 VM 當成容器載體,所以都選擇 Container-Optimized OS 這種映像檔
    • 大小:指的是叢集大小,也就是多少個叢集節點(主機數),此處選 3 是為了後續驗證負載平衡及應用程序崩潰時是否會影響服務之用
    • 其他參數暫時不需理會,直接按建立鈕即可開始建立容器叢集
  3. 花一點時間(聽說 Google 優化過了,現在來不及去泡咖啡了)讓容器叢集成功建立後就可在 Container Engine 資訊主頁中看到叢集的相關資訊
  4. 切換到 Compute Engine 資訊主頁中可以看到 3 個 VM 執行個體
  5. 可以直接在瀏覽器中連接 VM 進行操作,有瀏覽器就能進行管理(但好不好用要看平台,用手機相信是很難用就是了)
  6. 這是個 Kubernetes 專屬 VM,必然會看到一堆 Kubernetes 相關的容器在運作
  7. 有些網路工具像是 ping、traceroute 等等預設是未安裝的,可以透過執行 toolbox 指令另外弄一個容器來提供這些功能

安裝 Google Cloud SDK

Google Cloud Platform 雖然是以專案方式管理某項服務所需的各種資源,例如本案中的 Cloud SQL for MySQL、Container Engine 等等,但 Cloud SQL 和 Container Engine 之間並無互通,要讓 Container 能連接上 Cloud SQL 必須透過 Cloud SQL Proxy 進行轉接。目前這些設定步驟尚無法完全透過 Web UI 加以操作,取而代之必須使用 Google Cloud SDK 進行設定。

回到專案的資訊主頁,在入門指南這個資訊卡片下有一個『安裝 Cloud SDK』的連結,點擊它後即可取得各平台的安裝檔案(macOS 可以透過 Homebrew-Cask 安裝),請依指示完成 Cloud SDK 安裝。

初始化 Cloud SDK

安裝完 Cloud SDK 後需要進行初始化操作以授權存取 Google Cloud Platform,指令如下:
gcloud init
  1. 初始化過程中會透過瀏覽器進行線上認證
  2. 需同意 Google Cloud SDK 這些權限
     
  3. 指定預設的專案
  4. 設定 Compute Engine 預設的機房:應該是指若是透過 Cloud SDK 指令方式建立的 VM 都會以這個設定為準,這裡選 asia-east1-a
  5. 初始化完成後會提示目前登入的帳號、預設專案、以及預設機房
  6. 日後隨時可以檢視這些設定,也可以修改
    gcloud config list

管理 Cloud SDK 設定檔

若有多個 GCP 專案,則重覆執行 init 指令即可建立相應的設定檔。

檢視目前已建立的專案 / 設定檔,新建立的設定檔會自動啟用。
gcloud config configurations list

在多個設定檔同時存在的情形下,可以依需要啟用所需設定檔:
gcloud config configurations activate <proejct id>

不需要的設定設定檔可以刪除,但不能刪除啟用中的設定檔。
gcloud config configurations delete <proejct id>

使用 Cloud SDK 進行管理 Cloud SQL

前面使用 Google Cloud Console 資訊主頁的各項操作都可以透過 Cloud SDK 完成相關設定,但是 Cloud SDK 之所以重要是因為它能讓管理人員在任何地方皆能連接 Cloud SQL for MySQL,此特性讓維運人員即使是在 4G 環境下仍然可以隨時檢視 Cloud SQL for MySQL 的資料庫狀況。

檢查專案中資料庫列表:
gcloud sql instances list

連線資料庫進行管理,為了讓管理人員隨時連線,Cloud SDK 需要一點時間調整防火牆設定:
gcloud sql connect <db instance name> -u <user name>

設定 Cloud SQL Proxy

雖然覺得很蠢,但目前想要從 Google Container Engine 連接 Cloud SQL 服務的話,必須另外設定 Proxy,帶來的好處是應用程式開發時使用的 JDBC 資料庫連線都是指向本機 localhost 的,和一般開發過程中直接連線本地 MySQL 狀況相似。以下說明皆依據 官方說明文件 進行整理,並加入個人驗證經驗。

安裝 kubernetes 管理元件

透過以下指令可以協助安裝管理 kubernetes 所需的管理元件。
gcloud components install kubectl

將 Cloud SDK 使用中的憑證授權給 kubectl 使用。
gcloud container clusters get-credentials <叢集名稱>


以下指令可以檢視 Cloud SDK 目前支援的、已安裝的元件列表。
gcloud componets list

啟用 Cloud SQL API

請透過 這個連結 啟用 Cloud SQL 管理 API,也可以從 產品與服務 --> API 管理員 的資訊主頁中選擇啟用 Cloud SQL API。

建立服務帳號

Cloud SQL Proxy 需要一個服務帳號串接 Container 及 Compute 平台,請從 Console 的 產品與服務 --> IAM 與管理 --> 服務帳戶 功能選項中進入,或者直接點選 這個連結 進入前述資訊主頁(需再指定專案 ID)。預設會有一個用於連接 Compute Engine 的服務帳號。

點選資訊主賈上方的建立服務帳戶連結,填入相關資訊後按建建立鈕。

  1. 服務帳戶名稱:用來識別帳戶用途的名稱,隨便取
  2. 角色:請選擇 Cloud SQL --> Cloud SQL 用戶端
  3. 服務帳號 ID:若有需要可以自行修改
  4. 提供一組新的私密金鑰:請勾選,並且設定金鑰類型為 JSON,在帳號建立後會下載一個金鑰檔,請妥善保存;可以使用 SERVICE_KEY_FILE 這樣的變數去記錄保存位置,之後在匯入金鑰時會參照到

取得 Cloud SQL 執行個體連線字串

透過以下指令可以取得指定 Cloud SQL 執行個體的細節資訊,此處擷取的是它的連線字串,格式是:<專案 ID>:<區域>:<執行個體名稱>,此連線字串請標記為 CONNECTION_NAME 供後續使用。
gcloud sql instances describe <MySQL Instance Name> | grep connectionName


若是 Windows 平台那就去掉 | 及之後 grep 指令,直接用眼睛找一下  connectionName 這個關鍵字。

建立識別資訊

為了讓 Container Engine 可以連接到 Cloud SQL,必須將剛剛建立的 MySQL 用戶端服務帳號匯入 Kubernets 中讓 Cloud Proxy 可以參照到:
kubectl create secret generic cloudsql-instance-credentials \
  --from-file=credentials.json=${SERVICE_KEY_FILE}

編輯 K8S 部署設定檔

在專案目錄下有一個 k8s-deploy-config.yaml 的 kubernetes 部署 (Deployment) 設定檔,可以協助在 Container Engine 中建立一個應用系統部署單位。

請修改部署設定檔中的以下設定:
  1. 第 14 行的 Docker 映像檔標籤:請確認已打包成 GCP 線上版本所需設定,格式為:gcr.io/<專案 ID>/<映像檔檔名>:<標籤>
  2. 第 20 行的 Cloud SQL 執行個體連線字串

在 Container Engine 上啟動 Spring Boot 應用程式

完成前述設定後會得到兩個檔案:一個給 GCP 啟動用的 Docker 映像檔及一個給 Kubernetes 啟動部署作業所需的設定檔。第一件事就是將 Docker 映像檔上傳到 Google Cloud Storage 中。

上傳 Dcoker 映像檔 到 Cloud Storage

以下指令可將先前打包過的 Spring Boot 應用程式映像檔上傳到 Cloud Storage 中,其中 <映像檔標籤名稱> 也就是編輯部署設定檔時指定的 Spring Boot 時的映像檔標籤。
gcloud docker -- push <映像檔標籤名稱>


要確認是否上傳成功有兩個方式:
  1. 透過瀏覽器瀏覽 https://gcr.io/<專案 ID>/<映像檔名稱> (不含標籤)進行確認。
  2. 嘗試下載映像檔:若映像檔未正確上傳則會出現 not found 訊息
    gcloud docker -- pull <映像檔標籤名稱>

透過設定檔啟動 Kubernetes Pod

透過指定設定檔方式啟動 Pod。
kubectl create -f k8s-deploy-config.yaml

檢查 Pods 有沒有異常

Kubernetes 在啟動服務時可以透過以下指令,經檢視 RESTARTS 次數確認 Pods 啟動有無異常?
kubectl get pods

開放 Spring Boot 應用程式供外界使用

確認 Pod 沒有一直重啟後即表示相關設定沒有問題,可以正式發佈供外界存取。Kubernets 開放服務的指令是 expose,如下例:
kubectl expose deploy <deploy name> --port=<對外開放的埠號> --target-port=<Container 實際開放的埠號> --type=LoadBalancer

  1. <deploy name>:請參考啟動 Pod 時的輸出或是部署設定檔內的設定
  2. <對外開放的埠號>:這個服務實際要開放給外界連接的埠號
  3. <Container 實際開放的埠號>:在 Docker 映像檔中實際接受服務的埠號
當 service 被 expose 後,可以檢視該 service 的狀態 ,其中 EXTERNAL-IP 即為外界可以存取此服務的入口:
kubectl get services


實際連線可以看到 API 回覆了 Pod 的名稱:


若需要提供多個 Pods 以確保外界有機會繼續使用服務的話,可以擴充多個 deployment 。
kubectl scale deploy <deploy name> --replicas=<Pod 數量>


此時檢查 Pod 狀態會看到產生指定個數的 Pods:


經多次存取後,可以發現 Kubernets 確實將流量分散到不同 Pod 去,雖然分流的規則不明,而且還有很高機率會導向正在重啟的 Pod 去。


在此可以呼叫第二個  API 將 Container 弄壞,然後看看 Kubernetes 會不會自動重啟新的 Container 出來。

Kubernetes 日誌追查與異常排除

檢視 Pod 日誌記錄

kubectl logs -f <Pod Name> -c <Container Name>
  1. <Pod Name> 即 kubectl get pods 時取得的 Name 欄位
  2. <Container Name> 即部署設定檔中設定的 containers 設定中的 name 欄立
範例一:檢視 springboot 的執行日誌

範例二:檢視 Cloud SQL Proxy 的執行日誌

Spring Boot 無法連線 Cloud SQL

如果 Spring Boot 無法連線到 Cloud SQL 時必然會導致 Container 崩潰,此時的問題通常是因為先前的服務帳號沒有完整。請到 產品與服務 --> IAM 與管理 --> IAM 資訊主頁中確認先前建立的服務帳號是否被正確建立(包含該服務帳號的角色)。


若服務帳號沒有正確顯示在 IAM 資訊主頁中,請複製服務帳號內完整的 EMAIL 設定到 IAM 資訊主頁內自行新增即可,角色一樣請指定為 Cloud SQL 用戶端

與官方作法不同處說明

  1. 在 Cloud SQL Proxy 時並沒有建立 cloudsql-db-credentials 這個識別 (secret),這是因為這邊是由 Spring Boot 應用程式提供資料庫連線的帳密
  2. 若想將資料庫帳密放到 識別檔 (secret) 內的話,可以參考 cloudsql_deployment.yaml 這個範例設定部署檔

參考文件

後續的驗證與調整

有空的話會來驗證一下這些操作... 
  1. 如何更新應用程式版本:目前不知道是不是只要上傳新的映像檔即可?
  2. 如何真正達到服務不中斷:Kubernetes 預設的分流規則會把流量導向隨便一個  Pod,即使該 Pod 正在重建立,此時就會出現系統無回應的錯誤訊息
  3. 打開 Dashboard:據說檢視 Kubernetes 運作狀態好方便... ?

關於 Load Balancer

  1. 標準型(就上面那個例子)的 Load Balancer 是沒有做 Healthy Check 的,所以導致前面說到的經常性的把流量導到掛掉的那個 Pod ... XD
  2. Ingress 的 Load Balancer 似乎有相關的 Healthy Check ,還支援跨區域的分流 XD