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 物件,當回覆的 Optional 為 empty 時,才會觸發 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