2021年2月20日 星期六

智慧家庭中樞 Hubitat 入門:安裝與 Mobile App

去年感恩節的時候,趁著 Amazon 國際運費免運的優惠,買了一台智慧家庭的 hub。本來研究了一段時間是想買 Samsung SmartThings Hub 的,不過因為搞不懂版本的差異以及剛好又沒貨(那時正好因為 COVID-19 國際貨運運量低迷,可能也有關係吧),所以最後在搜尋了一些國外的評論後,改買了 Hubitat Elevation。


圖片節錄自 Hubitat 官網 [1]

2021年2月2日 星期二

[筆記] Spring 的 blocking 與 non-blocking 的效能差異?

本來在想要不要找個機會來玩玩 WebFlux,不過看了一下別人做的效能測試,看起來…好像找不太到明顯的動機…。雖然 Webflux 比 MVC 快不少,不過如果是跟 Async HTTP Client 比起來就沒有真的差太多,至少在這些商業邏輯並非真正的 bottleneck 的情況時,從 [1] 的數據看來好像沒什麼幫助….。

  1. Spring Boot performance battle: blocking vs non-blocking vs reactive
  2. API performance— Spring MVC vs Spring Webflux vs Go

2021年1月14日 星期四

Clean Architecture 入門

最近有機會重新架構一個新的專案,大家討論之後覺得想要試試 Clean Architecture 的架構,就稍微花了點時間學習了一下。不過因為只有粗略地翻過其中的一些章節,因此還只有在比較高層次概觀的理解,對於部份細節的實作還不是相當熟悉。這裡稍微紀錄一下目前為止理解的東西。

2020年12月4日 星期五

在 JMeter 做 Mutual TLS 認證

最近要跑壓力測試時,拿之前的腳本來改,結果改完後拋出 javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_required 的錯誤訊息,研究了一段時間,才發現應該是 mTLS 方面的問題…。

在手上已經有 key、certificate 以及 CA certificate 三個 PEM 檔案的情況下,如果是用 curl 的話,指令會長的像這樣:

curl --key key.pem --cert cert.pem --cacert cacert.pem $URL

不過在 JMeter 上,則有兩種作法。一種是如果 JMeter 有打開 GUI 的話,可以從 Options → SSL Manager 來指定。不過開 GUI 一般都只會用在還在寫 script 的階段,寫完正式要跑的時候,通常還是得用 CLI 模式,因此這時需要另外一種方法,也就是自己實際產生 Client Certificate 的 keystore 檔。

Keystore for Client Certificate

要產生 Client Certificate,需要用的工具就是 keytool,指令如下:

sudo openssl pkcs12 -export -name {name} -inkey {keyPath} -in {certPath} -out {keystorePath} -password {password} -noiter -nomaciter

其中各個參數的意義如下:

  • {name}:這應該是 key 的名字吧,在我們公司的狀況,這好像是必填,但在一般狀況似乎是可以不用給這個參數。
  • {keyPath}:key 的路徑。
  • {certPath}:certificate 的路徑。
  • {keystorePath}:要產生的 keystore 要放在哪裡。
  • {password}:keystore 的密碼。這裡有特殊的格式,如果是要直接在指令上指定,則格式必須是 pass:xxxx 這樣,例如如果密碼要設定為 12345,則要給 pass:1234。但由於這樣會讓 password 可能出現在機器上(像是 ps 可能會被看到),因此如果環境不是很安全的話,建議使用 stdin 這個值來手動輸入。

執行後,keystore 就會出現在指定的位置了。

啟動 JMeter

有了 keystore 後,因為 truststore 其實本來也有了(就是 CA certificate 那個 pem 檔),因此接著只需要在啟動 JMeter 時,讓它去吃那兩個檔就好了。其中如果你的 server 的 CA 本來就是公有的 CA,那可以不用自行指定 trustStore。

/bin/jmeter.sh \
  -D javax.net.ssl.keyStore={keyStorePath} \
  -D javax.net.ssl.keyStorePasswor={keyStorePassword} \
  -D javax.net.ssl.trustStore={trustStorePath} \
  -D javax.net.ssl.trustStorePassword={trustStorePassword}
其他相關資訊
  1. JMeter 腳本裡使用的 HttpClient 實作,按照 JMeter 官方文件 [3] 的講法,是必須要用 HttpClient,否則會無法支援 KeyStore 的 Client Certification。
  2. 如果遇到的錯誤訊息是 javax.net.ssl.sslexception: closing inbound before receiving peer's close_notify 的話,原因好像有可能是 server 和 client 支援的 protocol 對不起來之類的?總之此時如果你的 JMeter 環境是跑在 Java 11 上,可以試著退回 Java 8。
  3. 不想每次執行 JMeter 時都要指定 keyStore/trustStore 的話,也可以直接把它寫在 JMeter 的 /bin/system.properties 裡。
參考資料
  1. How to Set Your JMeter Load Test to Use Client Side Certificates
  2. Mutual authentication in JMeter
  3. JMeter – Component Reference – HTTP Request
  4. Spring Boot: Jdbc javax.net.ssl.SSLException: closing inbound before receiving peer's close_notify
  5. SSLException closing inbound before receiving peer's close_notify

2020年11月11日 星期三

在 Splunk 統計 log 量

因為最近需要縮減寫到 Splunk 的 log 量,因此需要先找出從哪裡寫的 log 量最大。要想找到哪邊是寫最多 log 的地方,初步的想法,就是先以 package/class 為單位,看看哪個 package/class 的 log 最多,就會有標的可以知道我們要從哪邊下手會最有效率。

要寫 Splunk query 時,先考慮一下 log 的格式。我們的 log 是單純地用 logback 輸出成 JSON 格式,然後就讓 docker log driver 送去 Splunk,所以 log 的 JSON 內容會放在 line 欄位裡面。因此在 Splunk query 中,package/class 會被放在 line.logger 中。另外如果數字很多的時候,要看出比例的差距也不是很容易,因此可以利用 eventstats 來幫忙計算整體的占比。

index="prod" source="mylog"
  | eval length = len(_raw)/1048576
  | stats sum(length) as package_size by line.logger
  | eventstats sum(package_size) AS total
  | eval percent = round(package_size/total*100, 2)."%"
  | table line.logger, package_size, percent
  | sort - package_size

結果如下:

line.logger package_size percent
com.a.a.a 37152.74659347534 46.89%
org.zalando.logbook.Logbook 28326.010219573975 35.75%
com.b.b.b 13741.930490493774 17.34%
com.c.c.c 8.518047332763672 0.01%
com.d.d.d 6.767726898193359 0.01%

內容稍微打個碼 XD。不過這樣結果就很清楚了,整體來說都是最上面那三個 class 在寫 log,而且其中第一名佔據了將近一半的資料量….。

不過需要注意的是,這個數字並不一定完全能夠代表這些 log 在 Splunk 裡佔據的空間量,因為 len() 指令是在計算字元數(character)的,當使用不同 encoding 時,一個字元換算成 byte 的大小會不一樣。

2020年10月23日 星期五

2020年10月19日 星期一

在 Vespa 使用 Parent/Child 時的限制

Parent/Child 的關聯在實務運用上挺方便的,可以讓有重複的內容統一放在 Parent document 上,使得 Child document 只要單純繼承 Parent 就可以一併繼承那些重複的內容。不過其實它相應的限制其實也頗多,而且這個部份在 Vespa 的文件上寫得並沒有非常清楚。

難以繼承複雜的結構

近期連續遇到的狀況就是,在 Search Definition 上定義欄位時不一定會出現錯誤,但結果實際上繼承是無法生效的。舉例來說,Parent document 上如果是 Array<StructA>,其中 StructA 裡面又包含了 StructB 的狀況時,結果是部署時並不會被檢查出錯誤,但實際上 StructB 無法正常地被 Child document 繼承到。

{
    "weapons": [{
        "type": "sword",
        "material": "iron",
        "price": 300,
        "position": {
            "longitude": 0.0,
            "latitude": 0.0
        }
    }, {
        "type": "sword",
        "material": "mythril",
        "price": 9000,
        "position": {
            "longitude": 0.0,
            "latitude": 0.0
        }
    }]
}

以這個例子來說,Search Definition 可能會長這樣:

struct Weapon {
    field type type string {}
    field material type string {}
    field price type double {}
    field position type map<string, string> {}
}

field weapons type array<Weapon> {
    indexing: summary
    struct-field type { indexing: attribute }
    struct-field material { indexing: attribute }
    struct-field price { indexing: attribute }
    struct-field position.key { indexing: attribute }
    struct-field position.value { indexing: attribute }
}

在這種狀況下,deployment 的檢查都會通過,但實際上 position 這個欄位是無法正常被繼承的。真的進行 Child document 的搜尋時,會發現 position 這個欄位一直都不會出現,但除了 position 以外的其他幾個欄位則都會正常出現。同時如果對 Parent document 呼叫 Document API,還是會看到包含 position 在內的所有欄位都正常地有值。

除了這個例子以外,另外嘗試在 Parent document 有非巢狀但格式有點複雜的欄位型態 map<string, array<string>> 時,結果則是在驗證階段直接拋出錯誤訊息:

For search 'childDocument', import field 'importedField': Field 'mapWithStringArray' via reference field 'weaponReference': Is not an attribute field. Only attribute fields supported

這個部份 Vespa 已經更新了文件 [3],稍微更詳細一點地說明了可支援的型態。可以參考 Vespa 的文件 [1-2]。

踩了幾次地雷以後,簡要來說,就是不要想在 Parent document 上擺什麼複雜的結構,除非那個欄位沒有要被 Child document 繼承,否則還是都用基本型別,最複雜大概就只到 array<struct> 或者 map<string, string> 這種就好了…..。

無法使用 index

因為 Parent document 是 global document,意味著它會被放在所有 content node 的記憶體中,因此它沒有辦法使用需要用到 disk 的 index。更確切地說,是 Child document 在繼承 Parent document 的欄位時,它只能繼承到被定義為 attribute 的欄位而已,index 的欄位無法被繼承。而這個限制也同時導致了繼承下來的欄位沒辦法做 partial match,只能夠作為 filter 來使用而已。

參考資料
  1. Parent/Child
  2. import-field
  3. Clarify restrictions that apply for imported field types.

2020年7月26日 星期日

解決 switchIfEmpty() 每次都被執行的問題

紀錄一下最近發現的有點意外的 Reactor 反應。

Mono.justOrEmpty(getXXX())
    .switchIfEmpty(fallbackXXX())
    ...

本來預期的行為是,getXXX() 會回覆一個 Optional 物件,當回覆的 Optionalempty 時,才會觸發 fallbackXXX() 去呼叫外部服務。但結果在測試時發現 fallbackXXX() 每次都必然會被執行,雖然最後結果會是對的,如果 getXXX() 有回覆時,走到後面的 stream 內容會是 getXXX() 回覆的東西,但這樣就等於每次執行時都會呼叫外部服務了,而且呼叫後拿到的結果還不一定會用到。

查了好一段時間之後,發現問題好像是出在 Java 本身的 method evaluation 行為上,然後解決方法是必須把 fallbackXXX()Mono.defer() 包裝起來,以達成讓 fallbackXXX() 推遲被執行的目的。

Mono.justOrEmpty(getXXX())
    .switchIfEmpty( Mono.defer(fallbackXXX()) )
    ...
參考資料
  1. Mono switchIfEmpty() is always called

2020年7月12日 星期日

Scala 的 HTTP 伺服器:http4s

在 Ubuntu 準備 sbt 環境

雖然覺得這個步驟蠻沒意義的,不過總之我又久違地在 Windows 上開了個 Ubuntu 20.04 的 container。

在安裝 sbt 之前,需要先加入 sbt 的 repository 到 apt 的來源。

echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo apt-key add
sudo apt-get update

接著就是安裝 Java 與 sbt。

sudo apt-get install -y default-jre sbt

以我目前這個時間點來說,會裝出來的版本如下:

Java: 11.0.7
sbt: 1.3.13
產生 http4s 範例專案

執行以下的初始化指令,讓 sbt 產生一個 http4s 的範例小專案。

sbt new http4s/http4s.g8 -b 0.21

這個指令其實本來在 http4s 的 Quick Start 裡是還有指定 sbt 版本的 -sbt-version 1.3.12,不過這裡就把它拿掉,讓它直接用剛裝的版本了。產生過程會問一些問題,這裡基本上我全都用預設值了。

接著就可以來看看產生出來的檔案有哪些內容了:

build.sbt

organization := "com.example"
name := "scala-test"
version := "0.0.1-SNAPSHOT"
scalaVersion := "2.12.4"

val Http4sVersion = "0.16.6a"
val Specs2Version = "4.0.2"
val LogbackVersion = "1.2.3"

libraryDependencies ++= Seq(
  "org.http4s"     %% "http4s-blaze-server"  % Http4sVersion,
  "org.http4s"     %% "http4s-circe"         % Http4sVersion,
  "org.http4s"     %% "http4s-dsl"           % Http4sVersion,
  "org.specs2"     %% "specs2-core"          % Specs2Version % "test",
  "ch.qos.logback" %  "logback-classic"      % LogbackVersion
)

HelloWorld.scala

package com.example.scalatest

import io.circe._
import org.http4s._
import org.http4s.circe._
import org.http4s.server._
import org.http4s.dsl._

object HelloWorld {
  val service = HttpService {
    case GET -> Root / "hello" / name =>
      Ok(Json.obj("message" -> Json.fromString(s"Hello, ${name}")))
  }
}

Server.scala

package com.example.scalatest

import scala.util.Properties.envOrNone
import org.http4s.server.blaze.BlazeBuilder
import org.http4s.util.ProcessApp
import scalaz.concurrent.Task
import scalaz.stream.Process

object Server extends ProcessApp {
  val port: Int = envOrNone("HTTP_PORT").fold(8080)(_.toInt)

  def process(args: List[String]): Process[Task, Nothing] = BlazeBuilder.bindHttp(port)
    .mountService(HelloWorld.service, "/")
    .serve
}

這樣就是個最簡單能跑的 Scala + http4s 的小專案了,可以透過 sbt run 啟動伺服器。執行後可以看到像是這樣的啟動訊息:

[info] Loading settings from plugins.sbt ...
[info] Loading project definition from G:\java\intellij\scala-http4s-test\project
[info] Loading settings from build.sbt ...
[info] Set current project to scala-test (in build file:/G:/java/intellij/scala-http4s-test/)
[info] Running com.example.scalatest.Server
[pool-6-thread-4] INFO  o.h.b.c.n.NIO1SocketServerGroup - Service bound to address /127.0.0.1:8080
[pool-6-thread-4] INFO  o.h.s.b.BlazeBuilder - http4s v0.16.6a on blaze v0.12.11 started at http://127.0.0.1:8080/

啟動完成後,就可以用瀏覽器連上 http://127.0.0.1:8080/ 了,例如範例的 REST API 網址會是 http://127.0.0.1:8080/hello/michael,因為輸入的 {name} 是 michael,所以就會獲得 {"message":"Hello, michael"} 的 JSON 回覆。

參考資料
  1. Installing sbt on Linux