2021年4月25日 星期日

Java 11 的 HashMap

在 Java 中,HashMap 是個挺常用的 Map 類別的實作。而且嚴格來說,雖然 Map 在 JDK 中就有 HashMap 和 TreeMap 兩大類(其實還有其他像是 Property 啦,但我個人覺得不太會把他們放在一起討論 XD),不過其實大多數狀況大家需要用到 Map 時,都不是太在乎 Map 是否有多餘的特性,只在乎 Map 這個介面原本提供的特性,也就是「任意一個 key 在 Map 中都是唯一的」,所以相較於 TreeMap 來說,HashMap 是比較常被使用的類別。這篇文章就是要來討論一下 Java 11 的 HashMap。

會想要寫這篇文章,其實是最近開始在準備面試,然後翻到以前寫的文章提到 HashMap 的時間複雜度的問題,才突然想起 HashMap 有個奇妙的特性!同時當時查資料時的對象是 Java 7/8,但現在的主流已經是 Java 11 了,也有些好奇 Java 11 的 HashMap 是否有其他的變化,因此就花了些時間再來閱讀一下 Java 11 的 HashMap 實作了(雖然 Java 17 LTS 也快出了....)。

2021年3月30日 星期二

JMeter 的 QPS 上不去

紀錄一下最近用 JMeter 做效能測試時遇到的小問題。

之前本來在測試效能時,因為會在測試環境進行,測試環境一般規模會比較小,所以相對地效能測試時打的流量也會比較少,大概都是打個 100~200 QPS 而已。不過這次遇到比較不一樣的狀況,我們需要在線上環境做測試,而且流量想要打高一點,結果就遇到 QPS 沒辦法往上拉的問題。

首先,測試的 script 裡設定的 Thread Group 是 1,000 個 thread,並且會用 Throughput Shaping Timer 來調整實際打出去的流量。script 跑在 AWS 上,使用的 VM 規格是 m5a.2xlarge。在 QPS ~300 左右時都還沒什麼問題,不過一往上拉到 350 QPS 以上時,就會開始出現 JMeter 顯示 response time 大幅地增加,但從 API 端觀察則看不出存在門檻,而且實際上我們日常的流量超過 350 QPS 很多…。

由於這個反應不太合理,畢竟我們平常實際運行的流量都高於 350 QPS,沒道理跑 350 QPS 的測試會卡住,更何況從 API monitoring 那邊觀察起來,API 的 response time 也很正常,並沒有如 JMeter 顯示的突然上升超過 20 倍,因此這時就開始懷疑有可能問題是出在 JMeter 這邊,像是來不及處理 response 之類的問題了。

接連做了幾個實驗、並且同時觀察 CPU 使用率後,發現一旦 QPS 超過 350 時,CPU 使用率會停留在大約 70% 左右的位置,這時多半 response time 會開始顯著地上升。因此初步認為有可能是 CPU 造成效能卡住了,而結果也確實是如此。在我把 VM 規格改為 CPU-intensive 的 c5a.2xlarge 之後,問題解決了,QPS 可以繼續往上增加,撞到了下一個門檻是大概 QPS 550…。

QPS 550 的這個門檻,觀察 CPU 使用率約莫是落在 50%~60%,看起來不太像是問題所在,因此又開始朝別的方向思考。CPU 不對的話,下一個可能就是記憶體了。此時觀察 top 顯示的整體記憶體使用率只有 2GB 出頭,但我的 c5a.2xlarge 應該有 16GB 可以使用才對,同時查到 JMeter 文件顯示預設的 heap size 是 1GB….。嗯~那就來調整 heap size 看看吧。

在 JMeter 資料夾中的 bin/jmeter 檔案裡,搜尋 HEAP 可以找到下面這一段描述:

# This is the base heap size -- you may increase or decrease it to fit your
# system's memory availability:
: "${HEAP:="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"}"

這裡顯示預設的 -Xms 和 -Xmx 都是 1GB,把它調大成想要的樣子,例如最少 8GB、最多 15GB,如下所示:

# This is the base heap size -- you may increase or decrease it to fit your
# system's memory availability:
: "${HEAP:="-Xms8g -Xmx15g -XX:MaxMetaspaceSize=256m"}"

然後~門檻就消失了,後續就順利地繼續把 QPS 往上拉到 1K 以上了。

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