2019年10月11日 星期五

為何在 Dockerfile 中使用 tail 是 anti-pattern

最近因為一個蠻莫名的問題,花了點時間在研究依靠 tail 來把 log 輸出到 docker log driver,然後發現其實這個作法算是一種 anti-pattern。目前認知到的問題有兩個:

1. tail 可能無法如預期地運作

在 stackoverflow 上,有好幾篇問題都做了類似的事情:

FROM debian:latest
RUN touch /var/log/mylog.log
CMD ["tail", "-F", "/var/log/mylog.log"]

在上面的 Dockerfile 中,先 touch 產生一個 log 檔,接著就一直 tail –F 去看那個 log 檔,感覺上好像很普通。但實際執行起來,會發現 docker logs 裡面什麼也沒有,tail 指令好像根本沒有正常在工作。而這個問題的根本原因是在於 docker 中間有 image layer,每一行 RUN 指令都會覆蓋上一層 image layer,因此實際上 touch 指令和 tail 指令在 docker 中是存取到不同的 inode。建議的作法應該要改為:

CMD ["sh", "-c", "touch /var/log/mylog.log && tail -f /var/log/mylog.log"]

也就是說,產生檔案以及監看檔案應該要在同一個 CMD 裡進行,這樣才能避免被 image layer 隔開而看不到預期的結果。

2. 若 container 的 main process 並非是主要的運行指令,container 可能無法對某些錯誤做正確的反應

這個問題主要是考量到能不能善用 container 本身的特性。在透過 Dockerfile 執行程序時,container 對程序的期待是如果程序執行完了,container 就會被認為不再需要了,於是 container 就會自動關閉。這個特性也可以用在如果程序執行有問題導致非預期地中斷,container 也一樣會自動關閉。此時如果外頭有 load balancer 的話,load balancer 就可以自動做一些反應,例如因為 container 數量減少了,因此自動做 scale-out 重新生出一個 container。但如果 container 的 main process 並非是在執行主要程序,而是在執行 tail 的話,那有可能會遭遇到主要程序出問題結束了,但 tail 沒有問題,這時就可能存在失敗的服務一直繼續留在那邊無法正確被關閉。當然這個動作也可以透過例如另外做一套 health check 的流程來處理,不過如果能夠很簡單地就利用 container 的天性來達成這個目的,也是可以考慮利用一下~。

參考資料
  1. Why I cannot get output of tail of a file created by docker build
  2. Output of `tail -f` at the end of a docker CMD is not showing

2019年10月1日 星期二

將 AWS ECS 的 log 導向 Splunk

花了好多天的時間在研究,然後終於稍微搞清楚想要將 AWS ECS 上的應用程式的 log 導去 Splunk 時,需要注意的地方有哪些,以及它導出時是怎麼做的。不過在紀錄之前要先提一下,這篇當中不會提到如何建置 Splunk 服務,因為在我的狀況中 Splunk 是其他公司內的團隊已經建好的。

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

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

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

其實很難看懂這到底在說什麼~。實際上,因為 Vespa 的 ranking 有分 first-phase 和 second-phase,其中 first-phase 會肩負減少資料量的工作,以確保當 Vespa 內存放的資料非常多的時候,不需要對所有資料都做完所有運算。而 keep-rank-count 是適用在 first-phase 階段的參數,造成的影響是當 Vespa 的 search node 在 first-phase 取資料時,最多只會依據 ranking profile 的計算去取出 keep-rank-count 指定數量的分數(和 docid),例如預設值 10,000 代表的是會暫存 10,000 筆資料的分數(和 docid),而超出這 10,000 筆資料的部份,分數就會被忽略(即只會留下 docid 被送進 second-phase)。因此走到 second-phase 時就只會有 10,000 筆資料能夠被後續做更複雜的處理。這也會反應在 Vespa 最後回覆的資料中,如果是超出 keep-rank-count 的部份的資料,分數都會顯示 –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