2019年9月28日 星期六

Logback 輸出 JSON 格式的 log

Logback 預設通常是會輸出一行一行純文字的 log,不過如果想要輸出 JSON 格式的話,也有看起來應該是第三方貢獻給 logback 的 extension 可以使用。

Maven 設定

<properties>
	<slf4j.version>1.7.26</slf4j.version>
	<logback.version>1.2.3</logback.version>
	<logback.contrib.version>0.1.5</logback.contrib.version>
	<jackson.version>2.9.9</jackson.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>${slf4j.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-core</artifactId>
		<version>${logback.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback.contrib</groupId>
		<artifactId>logback-jackson</artifactId>
		<version>${logback.contrib.version}</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback.contrib</groupId>
		<artifactId>logback-json-classic</artifactId>
		<version>${logback.contrib.version}</version>
	</dependency>
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>${jackson.version}</version>
	</dependency>
</dependencies>

除了 SLF4J 和 logback 本身以外,還需要 logback-json-classic,以及因為它使用 Jackson 來做 JSON 處理,所以需要 logback-jackson 和 jackson。

logback.xml 設定

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="JSON_STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
			<layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
				<timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>
				<timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>
		
				<jsonFormatter
					class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
					<prettyPrint>true</prettyPrint>
				</jsonFormatter>
			</layout>
		</encoder>
	</appender>

	<root level="ALL">
		<appender-ref ref="JSON_STDOUT" />
	</root>
</configuration>  

這裡很單純就設定了輸出時要用 JSON,並且其中的時間格式應該要符合我設定的格式,然後要求它輸出時要 pretty print(做好縮排等格式化)。

輸出範例

{
  "timestamp" : "2019-09-28T13:04:25.013Z",
  "level" : "TRACE",
  "thread" : "main",
  "logger" : "test.App",
  "message" : "test writing log.",
  "context" : "default"
}
參考資料
  1. JSON · qos-ch/logback-contrib Wiki · GitHub
  2. How to Log in JSON with Logback

2019年9月18日 星期三

Vespa 的 keep-rank-count 和 rerank-count

在文件 [1] 上對於 keep-rank-count 的描述如下:

How many documents to keep the first phase top rank values for. Default value is 10000.

而對於 rerank-count 的描述則是如下:

Specifies the number of hits to be re-ranked in the second phase. Default value is 100. This can also be set in the query. Note that this value is local to each node involved in a query.

其實很難看懂這到底在說什麼…。

事實上,它們的功能是應該要放在一起看待的。Vespa 的 ranking 有分 first-phase 和 second-phase,其中 first-phase 會肩負減少資料量的工作,以確保當 Vespa 內存放的資料非常多的時候,不需要對所有資料都做完所有運算。keep-rank-count 和 rerank-count 都是適用在 first-phase 階段的參數,以下分別討論它們的效果,會比較好理解。

keep-rank-count 造成的影響是當 Vespa 的每一個 search node (a.k.a. content node) 在 first-phase 取資料時,最多只會依據 rank profile 的計算去取出 keep-rank-count 指定數量的分數(和 docid),例如預設值 10,000 代表的是每個 nonde 都會暫存 10,000 筆資料的分數(和 docid),而超出這 10,000 筆資料的部份,分數就會被忽略(即只會留下 docid )。因此走到 second-phase 時就只會有 10,000 筆資料能夠被後續做更複雜的處理。這也會反應在 Vespa 最後回覆的資料中,如果是超出 keep-rank-count 的部份的資料,分數都會顯示 –infinite,代表因為分數已經在處理過程被丟棄,因此在結果中會不知道分數是多少。

rerank-count 則是決定離開 first-phase 進入到 second-phase 時,會留下多少筆資料。需要注意的是,這裡的數字一樣都是每個 search node 計算的。舉例來說,如果有 10 台 search node,rerank-count 設定為 100,那麼總共就會有 1,000 筆資料會被送進 second-phase。

keep-rank-count 和 rerank-count 兩個結合起來,假設我的 rank profile 設定是 keep-rank-count=100, rerank-count=500,同時現在有個搜尋的指令,會搜尋到 1,000 筆資料,那麼結果會是如何呢?…..結果會是 second-phase 會看到 500 筆資料,但這 500 筆當中,只會有 100 筆的 relevance score 存在,其他 400 筆的 relevance 會顯示 -infinite。

參考資料

  1. Search Definition Reference
​

2019年9月1日 星期日

Bloom Filter

當我們想要快速地知道某個資料是否曾經在系統中儲存過的時候,基本的作法就是時間複雜度 O(n) 或者是空間複雜度 O(n) 的選擇。

  • 時間複雜度 O(n):在存放的資料當中從第一個一直查到最後一個,花費 O(n) 的時間複雜度、同時沒有額外的空間複雜度需求,就能知道指定的某個資料到底有沒有存在。
  • 空間複雜度 O(n):存放一個配對表,例如如果資料是數字,那麼找配對表當中的指定位置,就能知道該資料是否存在。此時配對表需要花費 O(n) 的空間複雜度、但是帶來 O(1) 的時間複雜度優勢。

而 Bloom Filter 就是屬於中間型的解法,需要略高於 O(1) 的時間複雜度以及低於 O(n) 的空間複雜度。白話來說,就是 n 個資料並不需要長度為 n 的空間來存放,就能夠用來檢查 n 個資料中的任意資料是否存在於系統中。

概念上,Bloom Filter 就是使用一個陣列,用多個 hash function 來決定某個資料的位置,然後把陣列上的這些位置全都標為 1。其中資料不一定要是什麼格式,要求只是使用的 hash function 能夠輸出一個數字,來代表在陣列上的位置。因此檢查資料在不在系統中,只需要檢查這些位置是否全都是 1 就能夠知道了。不過因為是用 hash function,因此可以想見應該存在 collision 的問題,也就是有可能有多個資料會同時使用某個相同的位置,只是不會每個位置全都一樣。另外這個檢查方式也可以想像到潛在的問題:當檢查出某個資料的位置都是 1,有可能只是這些位置剛好都是被其他不同資料標上 1,並不一定真的表示我們要找的這個資料就確實存在。因此 Bloom Filter 無法保證某個資料一定存在(可能有 False Positive)、但可以保證某個資料一定不存在(不會有 False Negative)。

另一方面,因為 Bloom Filter 在儲存時只會存 0 和 1,表示它其實不知道每個位置是由哪個資料標上的。反過來說就是,當我因為加入資料的動作,把某個位置的值從 0 改為 1 後,日後我就再也無法把那個位置改回 0 了,因為 Bloom Filter 沒辦法知道這個位置跟哪個資料有關係。這也就形成 Bloom Filter 這個結構只能夠增加資料,而無法移除資料的限制。

所以簡要來說,Bloom Filter 就是個用比較少的儲存空間,快速判斷某個資料是否曾經被存放過的結構。

使用情境

以 Redia 文件 [4] 的範例來說,可以用來檢查像是帳號是否被使用過:

> BF.ADD usernames funnyfred
(integer) 1
> BF.ADD usernames fredisfunny
(integer) 1
> BF.ADD usernames fred
(integer) 1
> BF.ADD usernames funfred
(integer) 1

> BF.EXISTS usernames fred
(integer) 1
> BF.EXISTS usernames fred_is_funny
(integer) 0
參考資料
  1. [論文解讀][Bloom Filter] 深入討論 Bloom Filter
  2. 資料結構大便當:Bloom Filter
  3. 海量数据处理算法—Bloom Filter
  4. Redis: Bloom Filter Pattern

2019年8月25日 星期日

利用 Jackson 序列化、反序列化 JSON

JSON 的表示在資料傳送時很方便,不過在程式碼裡就不見得了,尤其像是 Java 這樣的強型別語言,要操作 JSON 常常得寫上一堆麻煩的程式碼。這也就是為什麼會有許多 JSON parser 的開源專案的原因~吧。以前我在做這些事情時都是用 GSON 來處理的,不過由於 GSON 到現在似乎還是不支援 setter、getter 形式的轉換,能夠操弄的手法比較少,因此就來做做 Jackson 的簡易實驗。

2019年7月28日 星期日

Vespa 的資料與索引型態

Vespa 裡面支援了許多資料型態(data type),包括 int、long、byte、double、boolean、string、struct(物件)、collection(例如 array、weighted set)等等,可以參考官方文件 [1],這些資料型態最主要的影響在於存放時所消耗的空間大小。而關於使用時的使用模式,則主要是來自於設定的索引型態(index type)來決定。

索引型態

索引型態主要分為 attribute 和 index 兩種。概略來說,可以直接把它們看成是「存放在記憶體」和「存放在硬碟」的差別。在 Vespa 的設計裡,好像基本上會假定字串以外的型態都放在 attribute 比較好的樣子,因為如果要以資料欄位作為像是排序的依據的話,資料欄位必須要是 attribute index 才行。

attribute

attribute 的欄位都會被存放在記憶體裡,意味著其實如果可以的話,最好不要把長度無法預測的資料欄位設定為 attribute,因為這樣會導致身為稀缺資源的記憶體被大量耗用在存放這些字串。不過實際運作時,因為 OS 會有虛擬記憶體的概念,因此不一定會真的完全保證 attribute 的欄位就一定是被放在記憶體上,如果想要確保這件事,首先就得確保 OS 不會使用到虛擬記憶體。

設定為 attribute 的欄位,就能夠執行以下的這些操作:

  • grouping
  • sorting
  • word-match search
  • numerical search
  • prefix search
  • ranking functions
  • document summaries
  • document reference

在對 attribute 的欄位做搜尋的時候,原則上 Vespa 只會做線性搜尋,只是因為搜尋時是對記憶體內的資料做,所以一般狀況下效率不會太差。但當有必要的時候,對 attribute 欄位設定 fast-search 可能是蠻重要的事情,這會讓 Vespa 對該欄位建立 B-tree 索引,就像在 RDBMS 上對欄位建索引一樣。到這裡,某種程度上可以感覺到 Vespa 對這種基本型態的處理其實算是比較粗糙的,效能很大程度是直接依賴「資料放在記憶體上」的這件事。所以如果沒有要做什麼比較複雜的排序或計算之類的話,基本資料的儲存並不是 Vespa 的強項,把資料放在 RDBMS 裡可能可以獲得相差不遠的效率但大幅下降的維運成本。

index

index 的欄位是存放在硬碟中的 [2-3],代表資料被定義為 index 的話,儲存成本相較於 attribute 會比較低,但同時存取速度也會比較慢。index 欄位只能用在字串上(這又突顯了 Vespa 對於基本型態的處理比較粗糙的事實),除了存放位置以外,index 跟 attribute 最大的不同就是 index 的欄位都會做 normalization 和 tokenization [3]。換言之,index 主要(唯一?)的使用情境是在於做各種字串處理或者字串搜尋。

Search Definition 的定義

在 Search Definition 上定義資料的索引型態時,attribute 和 index 並非是只能設定其中一個,其實可以同時設定兩個,只是同時設定時有個有點有趣的小細節。因為索引型態是透過 pipe 連接起來的,pipe 連接的意思就是會依序執行,所以它的結果會很像是使用 Java 8 的 stream API 那樣。舉例來說,如果我對某個欄位設定 indexing: index | attribute,這表示的是這個欄位的資料會先進行 index,在這個過程中會被 tokenize 等等,接著被 tokenized 的資料再送去 attribute 做索引!換言之,在一般狀況下如果我們想要同時設定 attribute 跟 index,通常我們應該會想設定 indexing: attribute | index,而不太會是反過來放的狀況。同時,當我們設定 indexing: attribute | index 時,Vespa 的搜尋會去搜尋 index 而不會搜尋 attribute [4]。(不過這樣的話那幹麻要支援同時寫 attribute 和 index 呀….?)

參考資料
  1. Search Definition Reference: field
  2. Proton: index
  3. Search Definition Reference: index
  4. Search Definition Reference: indexing
  5. Performance And Tuning: Document Attributes
  6. Performance And Tuning: Attribute Memory Usage

2019年7月21日 星期日

dependencyManagement 和 parent

最近專案上遇到一個奇怪的狀況,概略來說就是 @NotEmpty(javax.validation.constraints.NotEmpty)這個驗證無效。後來同事研究了好一段時間,發現問題好像是出在 dependency conflict。然後就做了一些 dependencyManagement 和 parent 的小研究~不過因為沒有很仔細在研究,所以也只是粗略紀錄一下的程度。

  1. dependencyManagement 指的是事先宣告 dependency 的參考,然後當後面在 dependencies 裡面有使用到宣告的 dependency 時,就會引用事先宣告的參考版本。換句話說,如果 dependencyManagement 裡面宣告了 dependencies 裡面沒有的東西,那就不會造成什麼影響。
  2. parent 是強制引入所有宣告在 parent 裡面的 dependency。因此不管專案自己的 dependencies 裡面有沒有使用到,parent 宣告的 dependency 都會被引入。
  3. parent 裡宣告的東西好像是強制引入,就算在 depepdencies 裡面想要覆寫版本也是沒有用的。不過聽同事說,如果是間接引用的狀況,例如 parent 裡的 dependency A 引用的 dependency B,就可以透過 maven 本身的特性覆蓋或變更~這點是還沒有確實實驗過。
參考資料
  1. Maven实战(六)--- dependencies与dependencyManagement的区别
  2. maven 中parent 与 dependencyManagement 
  3. What is the difference between “pom” type dependency with scope “import” and without “import”?
  4. maven如何不继承parent里面的部分依赖
  5. How to exclude artifacts inherited from a parent POM? 
  6. Spring Boot 不使用默认的 parent,改用自己的项目的 parent

2019年7月18日 星期四

Spring WebFlux:Reactive Programming(持續更新中…)

最近開始認真在讀 Spring WebFlux 的文件,所以稍微做一點筆記~。

何謂 Reactive?

在 Spring 的定義來說,Reactive 包含兩件重要的事情:「Reacting」以及「Non-blocking back pressure」。在 Reacting 的部份,Spring 的描述如下:

The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.

重點在於最後面的描述,我們現在處於「回應通知」的模式,在操作完成或者是資料可以存取的時候才採取反應。而對於 Non-blocking back pressure 的部份,則是以下的描述:

In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination.

這個在我以前一開始聽到的時候,其實不太能理解它的重要性,不過聽過很多次以後,慢慢地有了一點感覺~XD。在同步操作中,因為呼叫者會需要等待,所以呼叫者通常可以很明確地知道接收者什麼狀況開始受不了了;但在非同步的模式中,呼叫者跟接收者並沒有直接性的連接,所以基本上容易落入完全不知道對方狀況的情境,然後一股腦地一直發送事件,讓接收者被塞爆~。而在 Reactive Programming 的概念中,應該要包含接收者必須要具備 back pressure 的能力,有點像是在必要時壓制呼叫者,叫它停下來這樣。

Spring WebFlux 對於 Reactive 的支援

Spring 的體系中,現在分成兩大類:Spring MVC 和 Spring WebFlux,如下圖所示。兩邊都同時有支援 Annotated Controller 的模式,也就是可以用類似於過去的 Spring MVC 的方式使用 Spring WebFlux。不過如果要使用類似 Java 8 Lambda 的那種 Functional Programming 的 model 的話,就得採用 Functional Endpoints 的方式來開發了。


參考資料
  1. Web on Reactive Stack

2019年7月14日 星期日

Groovy 的 RESTClient 相關

忽略 HTTPS 的伺服器端檢查

restClient.ignoreSSLIssues()
回覆的 status code > 399 時,不要拋出 exception REST

Client restClient = new RESTClient()
restClient.handler.failure = restClient.handler.success
參考資料
  1. Is there an easier way to tell HTTPBuilder to ignore an invalid cert?
  2. Can't use ignoreSSLIssues in HttpBuilder version 0.7.1
  3. Can I override RESTClient default “HttpResponseException” response to >399 Return Codes?

2019年7月7日 星期日

Java 的 SSL 驗證

一般在 Java 要存取 HTTPS 的服務的時候,大多數狀況因為常見的 CA 應該都已經列在 JRE 的 cacerts(CA Certification 檔)裡了,所以通常不會遇到什麼問題。不過如果遇到要存取的是像是公司內部的服務,公司自己有自己的 CA 時,可能就得做點事情來讓 HTTPS 的 SSL/TLS 能夠正常運作。

概念上來說,當一個客戶端要存取 HTTPS 的服務時,客戶端只會相信它相信的對象。而要如何決定要相信誰?方法就是客戶端自己會存放一個信任的 CA 的清單,如果發現 HTTPS 服務的憑證是由信任的 CA 所發放的、並且憑證所保護的對象也確實是現在存取的對象,那客戶端就會相信這個 HTTPS 服務。以 Java 的狀況來說,客戶端指的是 JRE,JRE 內建的信任 CA 的清單會是 $JAVA_HOME/lib/security/cacerts 這個檔案。如果沒有特別指定的話,Java 程式在存取 HTTPS 時,進行 SSL/TLS 三方握手時就會以 JRE 內建的 cacerts 來決定對方是否能夠信任。

不過現實上,CA 還會分成 Root Certificate 和 Intermediate Certificate,例如 [1] 的範例一樣:

這裡 Entrust.net Secure Server CA 是 Root Certificate,DigiCert High Assurance EV Root CA 和 DigiCert High Assurance CA-3 則都是 Intermediate Certificate,最後 *.atlassian.com 這個網域的憑證是由 DigiCert High Assurance CA-3 所簽發的。客戶端若要驗證 *.atlassian.com 的憑證,它必須要信任上面的所有 CA(也稱為 Certificate Chain)才行。