小心 spring.jpa.open-in-view 導致 DB connection 被拿光

 spring.jpa.open-in-view

  • spring boot 的 property, spring.jpa.open-in-view 預設是開啟的
  • 開啟的話, OpenSessionInViewInterceptor 就會介入
    • 收到 web request 的時候, 會開一個 Hibernate Session
    • 如果用到 DB, 就會拿一個 DB Connection
    • 完成 request, 就把 connection 關閉


目的
  • 原本一個 entity 有像是 OneToMany 的關聯時, 預設都會 lazy.
  • 需要特別啟用這個 collection, 就需要特別去初始化, 例如 Hibernate.initialize
  • 如果沒有初始化就會遇到 LazyInitializationException


問題
  • 如果處理一個 request 的時候會需要比較長的時間, 那 DB connection 就會被卡住
  • BTW, 預設 DB connection 是 10條 ( https://github.com/brettwooldridge/HikariCP )
  • 如果需要呼叫外部服務, 或是外部服務遇到 connection timeout 之類的問題, 會導致一個 request 花很多時間才結束
  • 而導致 connection 很快被拿光, 而且無法拿到新的 connection 而爆炸..


怎麼辦?
disable open-in-view
  • spring.jpa.open-in-view, 把這個設成 false, 可以避免 connection 跟著 request
  • 可是要注意如果有 lazy 的地方要 initialize 否則會遇到 lazy exception
限制 request 的時間
  • 把時間可能拉長的設計跟 DB access 分開來
  • 例如把對外部系統的呼叫分開來

Reference
可能會需要調整 HikariCP 的 pool size, 參考: https://github.com/brettwooldridge/HikariCP

新團隊快速貢獻

前篇

前言
自從開始與新團隊合作後, 產品也即將 GA release.
GA 之後又會有新的不同的挑戰. 在新挑戰之前, 是時候紀錄一下這段時間發生的事情.

挑戰
  1. 產品本身
    1. 由於架構改變加上優化, 整個 backend 幾乎全部改寫, 而且加上支援 HA. 有大量還沒經過 QA 驗證的程式
      (全部改寫, 都只有 unit test, 所以算是全部都還沒驗證過 :P).
    2. 這個產品開發需要了解另一個產品才可以整合
  2. 人員改變
    1. 原本的核心團員一人
    2. 新外包團隊五個人加入, 四個工程師與一個 QA.
    3. Functional QA 與 Performance QA
  3. 同時間 (同個禮拜加入)
    1. 需要帶領新團隊五人從無到有了解產品並加以貢獻
    2. Functional QA 開始列 functional testcase 測試, 這是公司內經驗老到的 QA
    3. Scaling QA 開始列 performance criteria & testcase 開始測試效能, 這也是公司內經驗老到的 QA
  4. 時程
    1. 由於來支援的 QA 手上還有其他重要的事情, 所以我們不該耽誤他們太久
    2. 因此不論 scaling QA or functional QA 的時間都非常寶貴 (大家的時間本來就都非常寶貴)
    3. 必須特別注意不可以有 blocking issue, 有 blocking issue 就需要立馬解決

接受挑戰
規劃
Thread 1-1: 新團隊成員
  1. 協助裝機, 介紹開發環境
  2. 講解原理, 提供文件
  3. 拍影片, 介紹產品基礎設定與整合測試方法
  4. 下載程式後, 介紹程式架構, 依照 use case 介紹程式位子
Thread 1-2: Functional QA
  1. 介紹產品背景與 trade-off
  2. 解釋功能, 提供文件
  3. 討論 testcase 以及測試的範圍
Thread 1-3: Scaling QA
  1. 介紹產品背景與 trade-off
  2. 解釋功能, 提供文件
  3. 討論需要的 capacity 以及效能測試的方法
Thread 2-1: 新團隊成員
  1. 確認基本操作完成後, 開始安排 Functional QA 開的 bug中比較單純的部分交給新成員處理
  2. 由於每個人開始透過不同的 bug 來熟悉系統, 為了快速掌握進度, 開始進行 daily meeting
  3. 透過由淺入深的 bug, 來觀察還有甚麼地方不懂, 就一次講解給所有人聽, 期望盡早把知識都散布出去
Thread 2-2: Scaling QA
  1. 一開始 Scaling QA 遇到的問題最棘手, 因為效能大多就是測 critical path
  2. scaling test 的特性是: 一旦遇到一個量上不去, 測試幾乎就卡住了, 需要立即處理
  3. 力求每天 Scaling QA 遇到新瓶頸, 當天或隔天就可以有新的 release 可以繼續測試
  4. 由於新團隊成員掌握度還不高, 因此這類問題都要先讓原本的隊員與我來處理
Thread 2-3: Functional QA
  1. Functional QA 開的 issue 很多, 需要盡早判斷是屬於新團隊成員可以練習的, 還是需要與原本的隊員一起先解決的
  2. 新隊員摸索時期大多是從 NodeJS or Angular 起頭去看 application logic, 因此就先都安排在 NodeJS 或 UI 上可以看到程式的 issue 給新成員
  3. 如果跟 Linux system call 或是 IPv6 或是 Java/Groovy 做的比較偏系統操作的 bug, 就由我與原始隊員先排除
  4. 初期雖然大部分時間都在與 Scaling QA 合作, 但隨時要注意 Functional QA 有沒有遇到 blocking issue
Thread 3-1: 新團隊成員逐漸上手
  1. 上手後能協助的問題就變多了
  2. 定調 release 的節奏為每週一次, 簡化流程
  3. 開始把一些比較系統面的工作也指派出去
  4. 當遇到問題的時候就一起看
  5. 有經驗的人與我一樣先挑比較麻煩的 bug, 讓新團隊可以先熟悉 application code.
  6. 慢慢有些麻煩的 issue 試著交給新團員後, 有問題就分享平常解決問題的方式
  7. 阻止外部的人催促新團隊成員時程, 爭取按步就班上手的時間
Thread 3-2: Scaling QA
  1. Scaling test 尾聲, 與 Scaling QA 討論測試的上限
  2. 完成測試
Thread 3-3: Functional QA
  1. 持續把新成員有能力處理的 issue 指派過去
  2. 持續自行處理棘手的 bug

成就
終於, 在經歷了一百多個大大小小的 bug 後, 得到 GA candidate
  1. 通過 Scaling QA 安排的測試
  2. 通過 Functional QA test pass rate over 96%
過程中
  1. 原本的隊員與我在過程中一度需要去支援別的案子, 當下壓力頗大.
  2. 秉持著 "培養新團員就有 capacity 來處理更多 issue" 的精神, 繼續投資時間在與新團隊的溝通與分享上
  3. 最終果然有的新成員逐漸上手, 可以把更多事情安排出去, 討論的內容也愈來愈進階
  4. 時程快到之前, 每天都看著 scrum board 沙盤推演誰的 issue 做完之後可以提升 pass rate
  5. 直到有天 pass rate 超過了 96%

插曲
  1. 原本新團隊的隊長, 突然要離開公司, 搬回馬來西亞
  2. 新團隊的 QA 節奏有點跟不上, 協助幾次之後, 在與新團隊成員討論之後, 提出了 PIP, 最終我們好聚好散
  3. 新隊長會再找人補上空缺

後續
  1. 為了要 GA, 新團隊還沒有接觸較底層的東西
  2. 新團隊即將成為產品的下個版本的主力開發人員, 需要盡早掌握所有部份的技術
  3. 新上任的隊長還沒有帶隊經驗, 因此還需要協助建立新的 working model
  4. 也要協助新隊長理解新產品需求, 並規劃好 milestones. 讓團隊每個人都能 on the same page.

心得
時間一樣短短的 (2-3個月), 想不到列下來發生這麼多事情.
有人支援是很好的事情, 可是也需要注意釋放出來的知識需要按部就班規劃.
同時間要頂住壓力, 避免 QA 的 issue 壓太多給新隊員導致消化不良, 又要避免卡住 QA 浪費時間.
讓每個人都能處在剛好可以提出貢獻, 又有一點挑戰的狀態就很重要.
為了做到這點, 需要不停地從他人的角度來看待, 持續溝通, 想辦法讓人 on the same page.

很感謝不論是 QA, 原核心成員與新團員都很給力, 在許多混亂的資訊攤在檯面上的時候,
依然能一起持續討論, 找出有意義的目標與問題加以克服.
之後還有很新挑戰, 又是一輪新的動態搭配.

Dependency Inversion Principle 避免過度依賴外部

Introduction
DIP 應該很常見, 只是常常在談的時候會發現大家忘記了.
因此特別紀錄一下使用 DIP 實質上的好處.

Assumption
  1. 我們大多會希望 business logic code 可以乾淨穩定
  2. 有乾淨穩定的 business logic code, 就可以寫穩定的 unit test code
  3. 如此未來商業邏輯有改變的時候, 如果邏輯有衝突, 或是程式有問題很快就可以 flush

Reality
  1. 在沒有仔細思考的情況下, 分層式規劃結果無法有乾淨的 business logic code
  2. 例如, 假設 DB 是 PostgreSQL, MQ 則是 Kafka
  3. 很容易程式會出現 MyDomainService depends on MyDomainRepositoryHibernateImpl
  4. 這樣的架構就變成 business logic code depends on implementation
  5. 如此, 無法寫出穩定的 business logic test code, 因為都"必須"直接由 domain code 直接依賴 Hibernate & Kafka...
  6. 一個是需要準備 Hibernate & Kafka 的環境 or mock, 可能有些細微的調整沒藏好, 會導致一點小調整都可以讓測試壞掉


Solution
  1. 為了區隔出乾淨的 business logic code, 我們可以替 repository 提出一個 interface
  2. 如此, business logic code 僅止於 MyDomainRepository 以及 MyDomainMessageSender
  3. 同時, 也要注意 interface 上不可以出現底層的相關資訊
  4. 例如 MyDomainMessageSender 的 interface 不該出現 topic 這種 Kafka 概念的資訊 (除非我們自己訂一層 abstraction layer)
  5. 這時候就會看到為何像是 spring-data 會支援直接訂一個 interface 就可以透過 convention access DB


Besides
  1. 同樣的概念也可以套用在不同的 component
  2. 例如 application 跟很多外部系統界接, 所以程式裡面有分成不同的 component
  3. 透過 DIP, 可以把其他 system 的實作與主要的 business logic 獨立開來
  4. 如此就可以直接在測試描述對於外部喜統的 expectation, 而且不用為了 business logic test code 準備外部系統的細節
    (在目前的例子, 不用準備 gRPC and Kafka)





Spring Cloud Stream Introduction - 1

Goal
描述如何套用 spring cloud stream + Kafka 以及概念.
適合只面對 Kafka, 不涵蓋進階議題

Example

Concepts
  • 一個 application 的架構: 外部系統(middleware) -> input -> application process -> output -> 外部系統
  • 在 Spring Cloud Stream
    • 透過 Binder 來處理外部系統的細節. ex. spring-cloud-stream-binder-kafka
    • 透過 Binding 來處理 input.

Example
  • Setup
    • Spring Boot Application
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Kafka topic consumer
  • Consumer method (注意! 這裡的 method name: status 就是 Binding name, 會影響 application.properties)
@Bean
public Consumer<String> status() {
    return status -> System.out.println("Received " + status);
}
# 如果會從多個 topic 拿 message 就需要在 spring.cloud.function.definition 這個 attribute 指定, 並且用分號隔開. 這裡是預先指定一個
spring.cloud.function.definition=status

# 可以看到這的 pattern: spring.cloud.stream.bindings.{bindingName}-in-0.destination, 用來指定 topic name
# in 的部分是說 input topic
# 0 則是這個 binding 的第一個 input
spring.cloud.stream.bindings.status-in-0.destination=status
  • 再來就可以發訊息給 status 這個 topic

Function
透過 Function 可以處理 input -> process -> output
  • Function: 收到一個訊息後, 在尾巴貼上 random suffix, 然後回傳
@Bean
public Function<String, String> randomNumberSuffix() {
    return val -> val + " => append suffix " + Math.random();
}
  • 指定 input & output topic in application.properties
# 注意此時我們已經加上第二個 binding
spring.cloud.function.definition=status;randomNumberSuffix

# 指定 randomNumberSuffix 的 intput topic 是 randomNumberSuffix, output topic 則是 status, 也就是同一個 application 的另一個 binding
spring.cloud.stream.bindings.randomNumberSuffix-in-0.destination=randomNumberSuffix
spring.cloud.stream.bindings.randomNumberSuffix-out-0.destination=status
  • 發訊息給 randomNumberSuffix topic

Supplier
Supplier 是會被系統自動 polling, 預設一秒 poll 一次, 也可以指定 cron, 要 3.2 版之後才支援 custom binding polling configuration.
  • Supplier
@Bean
public Supplier<Date> mydate() {
    return () -> new Date();
}
  • 指定 output topic 給 mydate 這個 Supplier binding
# 此時已經增加第三個 binding definition
spring.cloud.function.definition=status;randomNumberSuffix;mydate

# 指定 output topic 為 status, 就是同個 app 的 topic
spring.cloud.stream.bindings.mydate-out-0.destination=status

# 改變預設的 poll config 為 2 秒 poll 一次
spring.cloud.stream.poller.fixed-delay=2000
  • Supplier 只要打開 app 就會自動被執行

Other Concepts
  • Consumer Group
    • 跟 Kafka Consumer Group 的概念一樣
    • 同一個 group 裡面只會有一個 consumer 收到 message
    • 不同 group 則都會收到訊息
    • 預設每個 consumer 都是不同的 group (anomymous group)
    • 透過 {binding}.group=xxx 來指定 groupName
  • Durability
    • 有指定 group, 則對 consumer 的 subscription 就會被保留, 即使這個 group 目前沒有 consumer, 等 consumer 回來, 就會接著收到訊息
    • anonymous group 的 subscription 就不會被保留, 因此 anonymous group 容易收到 duplicated message
  • Partition
    • 一個 topic 可以被切成多個 partition, 每個 partition 會由固定的一個 consumer 接收資料

 

別名演算法 Alias Method

 題目 每個伺服器支援不同的 TPM (transaction per minute) 當 request 來的時候, 系統需要馬上根據 TPM 的能力隨機找到一個適合的 server. 雖然稱為 "隨機", 但還是需要有 TPM 作為權重. 解法 別名演算法...