2026/3/16

Java Virtual Thread

Java 從 Green Thread 時代開始,演進到使用 OS Thread,然後到了 Java 21 版,正式採用 Virtual Thread,以適應微服務時代伺服器的變革。

Green Threads

  • JDK 為了跨平台的特性,要讓程式可以在單核心,沒有多個 thread 的 OS 上運作,所以設計了內建的 thread 跟 scheduler

  • 在 JDK 1.1 (1997) 時期,Java 使用 green threads(由 JVM 在使用者空間模擬出來的執行緒,不直接用 OS thread)。

  • 優點:跨平台,不依賴作業系統執行緒,能讓多執行緒程式在沒有多執行緒支援的 OS 上跑。

  • 缺點:

    • JVM 本身要實作 thread scheduler,效能比不上 OS 提供的 thread。

    • 遇到 blocking I/O(例如 read socket),整個 JVM scheduler 都會被卡住,所有 green threads 都會停住。

  • 從 JDK 1.3 (2000) 開始,Java 全面切換到 1:1 OS threads 模型

Native Thread

作業系統核心會用 time-sharing 或 優先權排程 來管理 threads。

  • 重量級資源單位

    • 每個 OS thread 需要 stack (通常 1MB 預設)、TCB (thread control block)、kernel data structures。

    • 建立/銷毀成本高 (微秒到毫秒級)。

  • 數量有限

    • 即使硬體支援很多核心,實務上 JVM 或應用程式能開的 OS thread 數量大約只有幾千到幾萬。
  • blocking I/O 會卡住 thread

    • 假設某個 OS thread 正在 read() 一個 socket,整個 thread 會被 kernel block。

    • 即使這時 CPU 沒事幹,這個 thread 對 JVM 來說就是「卡住不能用」。

  • 搶佔式排程 (preemptive)

    • OS scheduler 會把 CPU 切給不同 threads。

    • context switch 成本高,要切換 CPU 狀態、register、stack。

  • 共享記憶體

    • 所有 thread 共用同一個 address space,必須透過鎖 (lock, monitor) 控制同步,容易出現 race condition、deadlock。
  • OS thread 模型對「大量 I/O 密集型應用」來說效率很差

    • 適合少量、CPU 密集的任務

    • 不適合製作非常大量的網路連線伺服器

Virtual Thread

  • Project Loom 在 Java 19 引入預覽功能、Java 21 變成正式 GA 的一個重要新功能
  • 輕量級:建立/銷毀成本非常低,幾 KB stack 就能跑。

  • blocking I/O 處理方式不同

    • Virtual Thread 呼叫阻塞 API (Socket.read()),JVM 會攔截並讓出 OS thread。

    • 這樣 OS thread 可以拿去執行其他 Virtual Thread,不會浪費資源。

  • 數量級提升

    • 可以開數百萬個 virtual threads,對應數百萬個並發請求。

    • 讓程式碼還是同步/直觀,但效能接近非同步 I/O 模型。

  • 簡化程式碼

    • 不需要複雜的 callback、CompletableFuture、reactive pipeline,直接用同步程式碼就能寫出 scalable 程式。
  • 把「blocking I/O」變得非阻塞化,同時又保留傳統同步 API 的簡單性,讓 Java 更適合現代微服務/高併發應用。

  • 虛擬線程更適合 I/O 型或高併發場景。如果是非常 CPU密集或如果內部有同步鎖 (synchronized) 或重度共享資源競爭,則使用傳統線程。

Erlang Process

  • 極度輕量:一個 process 只佔用幾 KB 記憶體,可以同時開數百萬個。由 BEAM VM 調度

  • 獨立記憶體空間:每個 process 有自己的 heap/stack,不共享狀態。

  • 排程由 BEAM 虛擬機控制:BEAM 用 OS threads(通常一個核心對應一個 scheduler thread)去執行上千萬個 Erlang processes。

  • preemptive scheduling:Erlang process 執行一定數量的 reductions(指令數)後會自動讓出 CPU。

  • Erlang process 的定位 比較接近 Java 的 Virtual Thread

    • 因為 Erlang 從設計之初就為了 massive concurrency,整個 I/O 模型與錯誤隔離都是以「百萬 process」為目標;

    • Java Virtual Thread 是在既有 thread-based API 上加的輕量執行緒,強調「低成本封裝既有同步程式碼」。

Erlang Process vs Java Threads 比較

特性 Erlang Process Java OS Thread Java Virtual Thread
管理單位 BEAM VM (使用 scheduler threads) OS Kernel JVM (基於 OS threads)
重量級 / 輕量級 超輕量 (幾 KB) 重量級 (MB) 輕量 (KB 級)
建立數量 百萬級 幾千~幾萬 百萬級
排程 BEAM VM preemptive scheduling OS scheduler JVM scheduler
blocking I/O 不會卡住整個 VM,process 掛起,scheduler 跑其他 process 卡住 OS thread 掛起虛擬執行緒,釋放 OS thread
共享記憶體 不共享,透過 message passing 共享,需要鎖 不共享(但可用同步物件)
錯誤隔離 完整隔離,崩潰不會影響其他 process 線程崩潰可能拖垮 JVM 崩潰只影響該 virtual thread
設計哲學 為 massive concurrency 與容錯而生 傳統 multi-threading 保留同步 API,實現高併發
適用場景 聊天室、遊戲伺服器(每個玩家一個 process)、即時推播(pub/sub 模式) I/O 密集、RPC、WebSocket CPU 密集、JNI、非阻塞演算法

2026/3/9

Shell Script 語法比較表

整理常用 Shell (sh, bash, zsh) 在語法、功能上的差異與相容性,方便參考。

類型 sh (POSIX) bash zsh 備註
Shebang #!/bin/sh #!/bin/bash #!/bin/zsh 建議腳本跨平台用 #!/bin/sh
變數宣告 name=value 相同 相同 不能有空格
字串插值 "Hello $name" 相同 相同 都支援
命令替換 `date`$(date) 相同 相同 建議用 $( )
條件判斷 [ "$a" = "$b" ] [ "$a" = "$b" ][[ $a == $b ]] [ "$a" = "$b" ][[ $a = $b ]] [[ ... ]] 不是 POSIX 的寫法
邏輯運算 [ "$a" = 1 ] && [ "$b" = 2 ] [[ $a = 1 && $b = 2 ]] 相同 [[ ... && ... ]] 非 POSIX
數學運算 $((1+2)) ((i++)) / $((1+2)) ((i++)) / $((1+2)) POSIX sh 只能 $(( ))
陣列 ❌ 不支援 arr=(a b c)${arr[0]} arr=(a b c)${arr[1]} Bash 陣列從 0 起算,Zsh 從 1 起算
關聯陣列 key=value declare -A map; map[key]=val typeset -A map; map[key]=val POSIX sh 沒有
brace expansion {1..5} {1..5} POSIX sh 不支援
迴圈 for for i in 1 2 3; do ...; done for i in {1..3}; do ...; done brace expansion 會展開整個序列 {1..3} 不是 POSIX
函數定義 foo() { ... } foo() { ... }function foo { ... } 相同 function foo {} 不是 POSIX
字串長度 ${#var} 相同 相同
字串比較 = == / = == / = (glob) POSIX sh 只能用 =
大小寫轉換 ${var^^} / ${var,,} ${(U)var} / ${(L)var} Bash/Zsh 特殊功能
字串切割 ${var%pattern} / ${var#pattern} 支援更多:${var^^} (大寫) 支援更多::${(U)var} POSIX 只有 % #
printf / echo printf 標準,echo 不一定支援 -e echo -e 可用 echo -e 可能無效,用 print 建議用 printf
測試檔案 [ -f file ] 相同 相同
正則比對 [[ string =~ regex ]] [[ string =~ regex ]] 但 regex 語法不同 POSIX sh 無 regex 功能
展開 (globbing) 基本 * ? [ ] shopt -s globstar Zsh 預設更強大 (e.g. **/*.txt) Zsh glob 功能最強
補全 (tab) bash-completion 內建強大補全 互動環境差異,不影響 script
錯誤處理 set -e 相同,加強版 set -o pipefail 相同 pipefail 不是 POSIX
信號處理 trap 'cmd' INT TERM 相同 相同
source 檔案 . file source file / . file source file / . file POSIX sh 用 .
目錄堆疊 pushd / popd pushd / popd POSIX sh 無目錄堆疊功能
互動功能 readline、history、completion history、completion、prompt customization Zsh 提供最強互動功能
local / typeset local / typeset local / typeset POSIX sh 不支援函數內局部變數

  • sh
    • POSIX,相容性最好,但功能有限。
    • 如果要製作跨平台的 script,就使用標準的 POSIX 語法
  • bash
    • 增強 POSIX,支援陣列、關聯陣列、[[ ]]、brace expansion、Bashisms。
    • 一般在 mac/linux,可使用 bash script
  • zsh
    • 幾乎包含 Bash 功能,互動功能更強(prompt、補全、glob、history)
    • 陣列索引從 1 開始,部分語法行為不同。
    • 使用者互動操作時,可使用 zsh

2026/3/2

zsh

在 RockyLinux 測試 zsh。目前比較常見的,還是使用 bash,如果要測試 zsh,需要另外安裝。

安裝

dnf -y install zsh

修改預設的 shell

chsh -s /bin/zsh

基本的設定檔

可直接跳到下面的 Oh My Zsh

# 1. 基本環境
export LANG=en_US.UTF-8
export EDITOR=vim
export PATH="/usr/local/bin:$PATH"

# 2. Prompt 設定
PROMPT='[%*]%n@%m %~$ '

# 3. Alias 常用指令
alias ll='ls -lh'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'

# 4. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 5. 自動補全 & 修正
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全時忽略大小寫

# 6. 補全行為微調
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

功能有

  • 乾淨的 prompt[時間]使用者@主機 當前路徑$

  • 常用 aliasll, la, gs, ..

  • 歷史 → 儲存 5000 筆並共享不同視窗

  • 自動補全 → tab 補全,支援大小寫不敏感

  • 錯字修正 & auto cd → 輸入資料夾名稱會自動進去

Oh My Zsh

Oh My Zsh 是 zsh 常見的 plugin 管理工具,可安裝多個 plugin 擴充 zsh 的功能,但也要注意,載入越多 plugin 會讓 zsh 啟動變慢。

安裝

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

常見的 theme 工具是 Powerlevel10k,但這邊不使用,直接用基本的 Prompt

修改 ~/.zshrc 增加以下這個部分,然後就能將一些不同用途的設定檔分開

for file in .zshrc_*;
do source $file;
done

增加常用 plugins

安裝

cd $ZSH/custom/plugins
# 安裝 zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-autosuggestions.git

# 安裝 zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

# zsh-completions
git clone https://github.com/zsh-users/zsh-completions

設定檔

~/.zshrc_basic

# 1. 指定 Oh My Zsh 安裝路徑
export ZSH="$HOME/.oh-my-zsh"

# 3. Plugins
# git           → git alias & 補全
# zsh-autosuggestions → 歷史自動建議
# zsh-syntax-highlighting → 命令語法高亮
# history-substring-search → 部分字串歷史搜索
# extract       → 快速解壓縮
plugins=(
  git
  zsh-autosuggestions
  zsh-syntax-highlighting
  zsh-completions
  history-substring-search
  extract
  colored-man-pages
  z
)

# 4. 啟動 Oh My Zsh
source $ZSH/oh-my-zsh.sh

# 5. PATH & 編輯器
export PATH="/usr/local/bin:$PATH"
export EDITOR=vim
export LANG=en_US.UTF-8

# 6. Alias 常用指令
alias ll='ls -lh'
#alias la='ls -A'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'
alias gp='git push'
alias gd='git diff'

# 7. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 8. 自動補全 & 行為優化
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全大小寫不敏感
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

另外做一個 ~/.zshrc_prompt

#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f$ '
#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f${git_prompt_info} $ '
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats '(%b)'

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n@%m%f %F{magenta}%~%f${vcs_info_msg_0_} $ '

增加功能,區分本地跟遠端的 ssh

Zsh / Bash 都可以靠環境變數來判斷:

  • 本地登入 -> SSH_CONNECTION / SSH_TTY 不存在

  • 遠端登入 (SSH) -> SSH_CONNECTIONSSH_TTY 存在

if [[ -n "$SSH_CONNECTION" ]]; then
  HOST_STYLE="%F{red}@%m%f(ssh)"
else
  HOST_STYLE="%F{yellow}@%m%f"
fi

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n%f'"$HOST_STYLE"' %F{magenta}%~%f${vcs_info_msg_0_} $ '

重新登入後,就可以使用 zsh

extract plugin

extract 定義了一個名為 extract 的函數,用於解壓縮你傳遞給它的檔案,並支援多種檔案類型。

使用方式為 extract 檔案名稱

不需要知道具體的解壓縮命令,只需執行 extract 就可以解壓大部分常見檔案,直接輸入 x 檔案名稱 也可以

z plugin

Usage: z [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial
string ARGUMENT.

With no ARGUMENT, list the directory history in ascending rank.

  --add Add a directory to the database
  -c    Only match subdirectories of the current directory
  -e    Echo the best match without going to it
  -h    Display this help and exit
  -l    List all matches without going to them
  -r    Match by rank
  -t    Match by recent access
  -x    Remove a directory from the database (by default, the current directory)
  -xR   Remove a directory and its subdirectories from the database (by default, the current directory)

autojump

z 是用 shell script 實作,autojump 是 python,z 的速度比較快,且內建於 zsh。一般建議就直接使用 z,如果要使用 autojump,需要做另外的設定。

安裝

cd $ZSH/custom/plugins

# 安裝 autojump
git clone https://github.com/wting/autojump.git

注意 autojump 需要另外安裝套件

dnf -y install autojump

因為 autojump 只有提供 bash 版本的初始化 script

/usr/share/autojump/autojump.bash

現在要使用 zsh,故要另外產生一個 zsh 版本的初始化

到 autojump 原始的 github 網頁取得 autojump.zsh

把 autojump.zsh 放到 /usr/share/autojump/autojump.zsh

修改設定檔

plugins=(
  autojump
)

# Autojump 初始化
# 確認 autojump 安裝後再 source
if [ -f /usr/share/autojump/autojump.zsh ]; then
  source /usr/share/autojump/autojump.zsh
fi
# 任意切換目錄
cd ~/download/dir1
cd ~/download/dir2

# 檢查 autojump
j -s

# autojump
j dir1

2026/2/9

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>

2026/2/2

systemd template unit service

systemd template unit 是一種樣板服務 (service template),可以用同一份 unit 檔去啟動多個獨立的 service instance。當我們需要用同一個 service daemon 啟動多個 service instance 時,就可以透過這個方式,讓 service 對應到不同的設定檔,同時並存於一台機器中。

httpd

在 /usr/lib/systemd/system 目錄,除了 httpd.service,還有 httpd@.service

  • @ 代表這個 unit 是一個「模板」。

  • %i 代表實例名稱 (instance name),會在啟動的時候被替換。

systemd template 支援一些 specifier,常見的有:

  • %i → instance name (例如 site1 / site2)

  • %I → instance name,保持大小寫

  • %n → 完整的 unit name (httpd@site1.service)

  • %p → prefix name (httpd)

httpd@service 的內容是這樣

httpd@.service
# This is a template for httpd instances.
# See httpd@.service(8) for more information.

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target
Documentation=man:httpd@.service(8)

[Service]
Type=notify
Environment=LANG=C
Environment=HTTPD_INSTANCE=%i
ExecStartPre=/bin/mkdir -m 710 -p /run/httpd/instance-%i
ExecStartPre=/bin/chown root.apache /run/httpd/instance-%i
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND -f conf/%i.conf
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful -f conf/%i.conf
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed
PrivateTmp=true

service 會讀取 /etc/httpd/conf/%i.conf 設定檔,並將 pid 放在 /run/httpd/instance-%i

所以要產生兩個 httpd unit service 設定檔

cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site1.conf
cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site2.conf

修改 site1.conf 以下這些設定。site2.conf 就改另一個 Listen 8001,site1 改為 site2,去掉其他 Directory 的部分

Listen 8000
PidFile /run/httpd-site1.pid

DocumentRoot "/var/www/site1"

<Directory "/var/www/site1">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

ErrorLog "/var/log/httpd/site1_error.log"

CustomLog "/var/log/httpd/site1_access.log" combined

啟動

systemctl start httpd@site1
systemctl start httpd@site2

systemctl enable httpd@site1
systemctl enable httpd@site2

haproxy

如果是 haproxy,因為套件裡面沒有 unit service,我們需要自己製作一個

首先產生 /usr/lib/systemd/system/haproxy@.service 檔案

[Unit]
Description=HAProxy Load Balancer %i instance
After=network-online.target
Wants=network-online.target

[Service]
Environment="CONFIG=/etc/haproxy/%i.cfg" "PIDFILE=/run/haproxy-%i.pid" "CFGDIR=/etc/haproxy/conf.d.%i"
EnvironmentFile=/etc/sysconfig/haproxy.%i
ExecStartPre=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f $CFGDIR -p $PIDFILE $OPTIONS
ExecReload=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecReload=/bin/kill -USR2 $MAINPID
SuccessExitStatus=143
KillMode=mixed
Type=notify

[Install]
WantedBy=multi-user.target

製作設定檔

cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site1
cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site2

製作 /etc/haproxy/sit1.cfg

global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy-site1.pid
    stats socket /var/run/haproxy.admin.sock mode 660 level admin

    maxconn     50000
    maxconnrate 100000
    maxsessrate 100000
    user        haproxy
    group       haproxy
    daemon
    nbproc  1
    ca-base     /etc/pki/site1
    crt-base    /etc/pki/site1
    tune.ssl.default-dh-param   2048
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats-site1

    ssl-default-bind-options no-sslv3
    ssl-default-bind-options no-sslv3 no-tlsv11 no-tlsv10

defaults
    log global
    mode    http
    option  httplog clf
    option  forwardfor
    option  dontlognull
    option  httpchk
    option  http-keep-alive
    retries 3
    maxconn 50000
    rate-limit sessions 20000
    option  http-server-close
    timeout connect 1h
    timeout client  1h
    timeout server  1h
    #timeout connect 5000
    #timeout client  50000
    #timeout server  50000
    timeout tunnel  1h

frontend http_redirect
    bind    *:80
    mode    http
    acl kill_it method TRACE
    http-request deny if kill_it
    redirect   scheme https code 301 if !{ ssl_fc }
    default_backend web_server

frontend https_switch
    bind    *:443 ssl crt server.pem ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384
    mode    http
    option  forwardfor
    reqadd  X-Forwarded-Proto:\ https

    default_backend web_server

backend web_server
    mode    http
    fullconn    50000
    balance leastconn
    option      forwardfor
    #cookie      SERVERID insert indirect nocache
    #cookie SESSIONID prefix indirect nocache
    cookie  SESSIONID prefix nocache
    http-request        set-header X-Forwarded-Port %[dst_port]
    http-request        add-header X-Forwarded-Proto https if { ssl_fc }
    #option      httpchk GET /
    option  httpchk *
    server  W01 localhost:8000 weight 10 check cookie W01 inter 5s rise 2 fall 3

製作另一個設定檔 /etc/haproxy/site2.cfg,注意要修改 bind port

然後注意,申請兩個 ssl 憑證,放到 /etc/pki/site1 跟 /etc/pki/site2

啟動

systemctl start haproxy@site1
systemctl start haproxy@site2

systemctl enable haproxy@site1
systemctl enable haproxy@site2