文章列表

Swarm mode 上線 5 - load balancer | proxy gateway 代理伺服器

科技新知
MacauYeah・2024-11-11

前面的例子,我們已經成功設定 ingress Network,也加了 virtual ip 。如果大家的目標是單一 web 應用,應該就已經很足夠。但作為一個足夠節儉的老闆,怎會讓一個 Swarm 只跑一個 Web 應用?但問題來了,一個 docker swarm service 就已經佔用一個公開端口 (例如上述的8888,或是更常見的443)。怎麼可以做到多個 service 分享同一個端口?答案就是回到傳統的 Web Server 當中,使用它們的 virtual host 及 proxy 功能,以達到這一效果。我們就以 Nginx 為例,去建立一個守門口的網關 (gateway) 。 以下就是一個最簡單的例子,最前端的 http-gateway (nginx) 對外公開端口 8080 ,它根據 virtual host,去分派對應的請求去 dmzhttp (bretfisher/httpenv) 及 managerhttp (bretfisher/httpenv) 。構架圖就是以下這樣。 ┌───────────┐ ┌──────────────►│ dmzhttp │ │ └───────────┘ │ ┌───────────────┐ │ http-gateway │ ────────►│ (nginx:8080) │ └──┬────────────┘ │ │ ┌─────────────┐ └─────────────►│ managerhttp │ └─────────────┘ 換成 docker stack ,就大概如下 services: http-gateway: image: http-gateway ports: - 8080:8080 deploy: replicas: 1 update_config: delay: 10s restart_policy: condition: on-failure dmzhttp: image: bretfisher/httpenv deploy: replicas: 2 update_config: delay: 10s restart_policy: condition: on-failure managerhttp: image: bretfisher/httpenv deploy: replicas: 3 update_config: delay: 10s restart_policy: condition: on-failure docker stack有一個很好的功能,就是 service 名會自動成為同一段網絡中的 hostname 。即是http-gateway中,它可以經DNS,找到 dmzhttp 、 managerhttp,也就是它的 nginx 可以設定成如下的樣子。 # default.conf server { listen 8080; listen [::]:8080; server_name managerhttp; resolver 127.0.0.11 valid=30s; location ^~ / { set $upstream_manager managerhttp; proxy_cache off; proxy_pass http://$upstream_manager:8888$request_uri; } } server { listen 8080; listen [::]:8080; server_name dmzhttp; resolver 127.0.0.11 valid=30s; location ^~ / { set $upstream_dmz dmzhttp; proxy_cache off; proxy_pass http://$upstream_dmz:8888$request_uri; } } 上面的例子中,就是一般的 virtual host + nginx proxy 設定。特別要說明的是 resolver 那一行,它指向 docker DNS (127.0.0.11), 而且還可以讓nginx在找不到上游時,不要馬上死亡。這樣 docker swarm 中各個 service 隨時加加減減,有保命的作用。 最後我們的 http-gateway 就是 nginx image + default.conf 上述的 docker 就可以用以下方式打包。 # Dockerfile # docker image build -t http-gateway ./ FROM nginx:latest COPY default.conf /etc/nginx/conf.d/default.conf 上面的 docker stack 和 nginx config,只要同步增加 service 及對應的 proxy pass,就可以o讓同一個端口,根據不同hostname做分流。當然,如果大家可以共用端口及 hostname 也可以,分流就改用 nginx location 來設定,不過這是更加偏向 nginx 的內容,日後有機會再介紹。本篇就先集中於 docker 相關的議題。 在安全性的角度, docker 還有一些配置可以做,就是讓 dmzhttp 和 managerhttp 在不同的機器上發佈。假設我們的網絡分開兩段,一段是 manager 專用,一段是 dmz 專用。在建立 docker swarm 後,我們可以為不同的節點加入對應的標簽。 docker node update --label-add zone=manager YOUR_MANAGER_NODE docker node update --label-add zone=dmz YOUR_DMZ_NODE 然後我們通過修改 docker stakc 中的 placement -> constraints ,限制不同的 service 在不同的節點上運行。 services: http-gateway: image: http-gateway ports: - 8080:8080 deploy: replicas: 1 update_config: delay: 10s restart_policy: condition: on-failure dmzhttp: image: bretfisher/httpenv deploy: replicas: 2 update_config: delay: 10s restart_policy: condition: on-failure + placement: + constraints: + - node.labels.zone==dmz managerhttp: image: bretfisher/httpenv deploy: replicas: 3 update_config: delay: 10s restart_policy: condition: on-failure + placement: + constraints: + - node.labels.zone==manager 使用上面的例子,我們就可以達到簡單分離的效果。但大家緊記,這個分離效果始終是一個規則式功能,它與防火牆的隔離還是有本質上的區別。除了利用傳統的防火牆技術外,我們的docker swarm network,其實也可以做更多隔離,我們日後再慢慢加強這個例子。

Git 歴史修正

科技新知
MacauYeah・2024-10-29

有時候,我們修正一系統檔案,例如某個commit中,多了一個不該放的檔案,又或者想修改該commit的作者,我們就要追搜到某個commit,然後用rebase隨個改。 例如本次repo,有一個githubAction.md,因為錯誤原因,被加到了main中,也藏了很久。如果我們想連根拔起,我們需要加出它第一次出現的commit。 $ git log githubAction.md commit 60ccd70f6b768138cbe23c93ffcfa32574ce895c 那我們就以它前一個commit作為rebase的根據,進行逐個commit修正。 $ git rebase -i 60ccd70f6b768138cbe23c93ffcfa32574ce895c^ pick 60ccd70 draft some content pick e2ee9a3 add some senario. pick b91afc1 refine submodule; pick 98cd366 add notes about submodule specific checkout; pick 064b06f test directly commit in submodule main pick 7b648d2 update git submodules notes pick 556f25e add notes about merge timing pick 5244804 Create git-continuous-integration-strategy.md pick 107e486 add more pratical nodes about ci; pick d93cbee add mono repo challenge pick 1c471b6 add worktree notes pick 9063ccb notes about different of git flow and github flow; pick b72e89e Update github-flow.md, add ref more link pick 0b8f2a9 draft github flow release problem pick 8b333fc finalize github flow release strategy 在rabase選項中,把需要改的commit由pick改為edit。(rebase會以舊到新顯示)。然後儲存。例如 edit 60ccd70 draft some content edit e2ee9a3 add some senario. edit b91afc1 refine submodule; pick 98cd366 add notes about submodule specific checkout; pick 064b06f test directly commit in submodule main pick 7b648d2 update git submodules notes pick 556f25e add notes about merge timing pick 5244804 Create git-continuous-integration-strategy.md pick 107e486 add more pratical nodes about ci; pick d93cbee add mono repo challenge pick 1c471b6 add worktree notes pick 9063ccb notes about different of git flow and github flow; pick b72e89e Update github-flow.md, add ref more link pick 0b8f2a9 draft github flow release problem pick 8b333fc finalize github flow release strategy 我們第一次會在60ccd70,我們作出想要的改動,然後經amend去改掉60ccd70 $ rm githubAction.md $ git add -u $ git commit --amend --author="newuser " 確定無誤的話,就可以去下一步,即是到了e2ee9a3 $ git rebase --continue 因為已經rebase過,你此時看到的不會再是hash不再是e2ee9a3,而是自動rebase完的e2ee9a3。若大家有東西要改,就使用commit --amend。如果沒有東西要改,也沒有conflict,可以繼續rebase --continue下去。

Swarm mode 上線 5 - load balancer | 負載平衡器

科技新知
MacauYeah・2024-10-28

前面我們一直談 swarm 的設定,但對於真實的服務,我們還要考慮客戶端是如何連接我們的伺服器群集。通常網路服務,客戶端都會經過域名轉換成IP,然而通過IP連線服務。 Ingress Network 假設我們 swarm 內有5個節點,那到底域名應該指向我們哪一個節點的 IP 呢? 如果我們不考慮節點死機的話,其實5個節點的IP都可以。因為 swarm 會自動把同一個公開的 port ,在每一個節點上都可以訪問到。 以下例子,即使只有一個 container 運行,佔用 port 8888,它還是會在5個節點上全開。 swarm 通過自己的 ingress network,它所有節點的 8888 串連起來。 services: http: image: bretfisher/httpenv ports: - 8888:8888 deploy: replicas: 1 update_config: delay: 10s restart_policy: condition: on-failure 我們可以在每個節點上,都會找到這個 ingress network,而且那個Network ID,應該是一樣的 > docker network ls | grep ingress t7rmk6g9zybm ingress overlay swarm 如果上述的 service 的 replicas 調成大於1的數量, ingress network 還會方便地自動 round robin (輪替) 地分派流量,達到最簡單的負載平衡。 Virtual IP 前述的設定,我們有一最大的假設,就是節點不會死機。但實際情況下,各種原因,例如安全性更新、重啟中,都會讓節點暫時無法使用。即使所有 service 都是會自動 failover (故障轉移),但客戶端還是用舊機 IP ,它還是無法訪問。因為該機 IP 已無法使用,除非我們連 IP 也懂 failover。這時, Virtual IP 就是我們的救命靈藥。 在 ubuntu 上,我們可以經過 keepalived 去設定 Virtual IP apt-get update && apt-get install keepalived -y 然後設定 keepalived , 假設 172.22.1.5 是我們的 Virtual IP 。 然後每個節點都要加入conf # vim /etc/keepalived/keepalived.conf # assume failover ip is 172.22.1.5 vrrp_instance VI_1 { # change interface according to machine status interface eth1 state MASTER # 101 for node1, 102 for node2 # you can start seq from other value, remind unqiue for each node is ok; virtual_router_id 101 # lower value will become master # ex, node1 priority 100, node2 priority 200, node3 priority 150. # if node 1, 2, 3 alive, node2 will become master. # if node 2 gone, node 3 will become master. priority 100 advert_int 1 authentication { auth_type PASS auth_pass YOUR_RANDOM_PASSWORD } virtual_ipaddress { 172.22.1.5 } } 上述需要特別注意的是 virtual_router_id : 每個節點應該都要不一樣,以作唯一標識。 priority : 每個節點應該都要不一樣,最大的那個節點,就會優先使用 Virtual IP 。 auth_pass : 每個節點都相同,但大家在抄時,記得更改。 還有的是開通 iptables ,讓各個節點可以經網絡廣播的方式互相看到對方。 iptables -I INPUT -d 224.0.0.0/8 -j ACCEPT iptables -I INPUT -p vrrp -j ACCEPT systemctl restart keepalived

Docker Tag 命名

科技新知
MacauYeah・2024-10-24

一般來講,同一個docker image會提供多個不同的版本,每個版本會附予不同的tag,以作標識。但以docker image的維護者來講,它的tag通常代表的是自己程式的版本號。不過這個版本號卻存在很多變數,就讓筆者好好地逐一說明。 程式的版本號 在沒有Docker的年代,其實所有軟件在發佈時,都會標示版本號,方便使用方明確追蹤問題,自行選擇升級、降級以解決相容性問題。大家要重現問題,也能清清楚地重現。所以docker image的tag,在某程度,都是代表發佈自己的程式版本號。但以前的年代,軟件底層的依賴,例如OS層面的共享程式庫,則不在發佈的管控中,所以過去的程式,在跨電腦安裝時,都會出現缺少某些共享庫的問題。而使用了Docker後,image以內的共享庫的都會在打包的那一刻固定和發佈,就不會有漏的問題。 庫更新,怎麼辦 上面說到image可以打包共享庫,但問題是共享庫也會有安全性更新問題,那麼對docker image的維護者來講,它自己的tag又該如何命名? 因為庫的量可大可少,所以一般來說,都不可能完全把各個庫的版本號寫在自己的tag上。退而求其次,就是用"版本號+日期",庫的細版本號,就存在原始碼當中。Ubuntu 就是這樣的例子。 不過"版本號+日期"的命名方式真的方便嗎?每次下遊用戶想更新去最近版本,都要自己找一次最近的日期。這樣對很多用戶來講都不夠方便。所以docker又提供了一個重tag的功能。例如ubuntu:noble,在早些時候指著noble-20240904.1,然後過幾天,又指向更新的noble-20241009。更常見的是latest,每次image都預設會存在,docker也希望大家會定期更新這個tag,讓大家可以更易地找到最新版本。 註: 這跟git tag有所不同,git tag並不預期會變的。當協作者收到tag後,那怕上遊刻意更新tag指針,協作者沒有刪除原tag之前,都不會知道tag更新去了哪裏。 我們該如何選 在發佈方和引用方來講,引用時可以明確使用唯一的"版本號+日期",對穩定性來講是有意義的。不過多多少少,會產生額外的時間成本。發佈方來說,就是多用了一些儲存空間,方便引用方可以隨時找到舊(庫)版本。而引用方,就要手動修改引用號,作為驗收依據,自動更新的難度比較大。 但對於自動更新要求比較大的情況下,可能就是使用latest或者會隨時更新的share tag(共用tag)比較實際。但我們也依然要定一些方式去版本更新記錄,例如:同時使用 beta latest archive 每日自動更新beta,只有所有測試都通過時,才把archive指向現在的latest,再把latest指向現在的beta。這樣做的好處是,核心的docker stack檔案改變的機會較少,也可以免除docker swarm做太細緻的權限管理。

重入膠坑

手機‧電玩
MacauYeah・2024-10-21

上期講到,筆者因為機緣之下,認識到賢者模型工作室,也很感謝他們幫忙修復了筆者那斷椿的模型(斷樁是膠老永遠都要面對的難題) 不知道大家看了筆者的爛尾舊模之後,有沒有也心動想把自己的舊模拿出來再繼前緣?概然有這個麼好的地方,有人指路,何不帶著大家的模型或問題,去賢者問一問。筆者就在最近一個月,就乾脆一口氣做一隻新模,翻新一隻舊模。 筆者也來分享一下最近的素組+補色的制作心得 剪鉗 - 新模 如果大家有機會做新模,看大家有沒有打磨的需求。打磨是很花時間的,但成品通過打磨後,即使不上色,只噴保護漆,已經超級美觀的。但時間成本也真的很高。 為什麼明明標題講剪鉗,但開篇卻在講打磨。因為第一步把模型從流道剪下來的那一刻,就間接地影響了日後能不能打磨。也根據大家要求的打磨完美度,去決定剪鉗要買多貴。 不論大家想不想打磨,但買一把實用的剪鉗,不要在剪下來取件的一刻令水口位花了,是很重要的。雖然有時留了位置打磨,但若水口花了,有時還是會傷到零件本身,不論表面怎樣打磨,都救不了。那時不是使用補土,就是要打磨到缺肉。家裏環境不方便上色的話,補土的做法就不適合了,還是好好地選剪鉗吧。 完全不打磨,可以買薄刃單刃剪。薄刃單刃剪可以極大地讓水口切割平整,又讓水口細小,之後不想打磨,也不會大刺刺地看到。筆者有試過這個方案,筆者還有一些更奇妙的想法:一開始開心剪,先素組/假組再說,後期有機會再打磨。但這個不是賢者老闆的推薦方案,而筆者慢慢地也發現了一些問題,果然不聽老人言,吃虧在眼前。我們有機會實際操作再詳述。 進行打磨,可以買田宮的雙刃剪,雙刃剪也有薄刃的,但鋒利程度不單刃。賢者老闆比較推這款,因為價格宜人,也耐用。不過筆者都有聽一些其他朋友的建議,一把粗剪,一把薄刃,這樣薄刃剪比較耐用。因為薄刃剪不論多便宜,也要 MOP 200起跳。但壞處就是時間成本越來越多,所以到頭來,可能賢者老闆推薦的田宮的雙刃剪 + 自行打磨,才是長久的做法。 打磨 - 新舊模 若果大家選擇打磨的話,就需要不同號數的砂紙。筆者就直接買了老闆推薦的海棉砂紙套裝:400, 600, 1000, 1500, 3000。省心,好用。 不過對於有需求的朋友,可能要另外再散買800和1200號。另外400,600號,也要加構砂紙+打磨板的版本。因為400,600號切削力強,不分情況全用海棉的話,很易磨到出現曲面。800和1200則作為600、1000、1500之間的過渡。有時太粗心,部份地方沒有足夠用1000打磨,噴保護漆後,還是會有明顯的傷痕。 手塗補色 - 新舊模 不論大家有多痛恨Bandai的貼色,筆者都不建議大面積手塗補色。因為市面上手塗的marker筆,多為水性塗,附著力本來就低,沒有底色補土的情況下,水性塗很易刮塗。筆者之前不知道,刮了又塗,塗了又刮,惡夢。經向賢者請教之後,才放棄這一方向。 而中、細型面積的補色,軟頭的Marker補色筆,實在太好用。除了白色這類遮蓋力不強的顏色,其他遮蓋力強的顏色,例如藍色、紅色,在補土加成之下,筆者塗得很滿意,不過刮漆還是會有。

Tmux - 繼 Screen以後的Linux多工神器

科技新知
MacauYeah・2024-10-08

因為各硬件/軟件的發難,筆者又不得不回到那個只有純純linux tty console的世界。很多時候,那怕使用tty,我們在Desktop mode,也有現代terminal 可以用,需要多分頁,滑鼠選取文字、複制、貼上,都可以輕易做到。 但在mobile / tablet device 上,手指操作真的很不方便。又或者你像筆者一樣,即使有電腦,但要操作一些Linux VM,它們連ssh都沒有,只能直接登入它們的tty,那麼懂得使用Tmux進行分頁及複制、貼上,就變得很重要。 Tmux 是什麼? Tmux 就是可以在Linux Terminal 同一個窗口中,實現多工處理的小程式。就像我們利用多分頁一樣,不同分頁做不同的事。不過最大的差異就是,生成分頁,排列分頁,我們都要使用鍵盤來完成。有時筆者也會用它來作為背景程式,以免不小心關了Terminal就會把所有運行中的指令都停掉。 我們就馬上來看實際例子吧 前置事項: 安裝Tmux及運行Tmux Debain & Ubuntu 安裝: sudo apt-get update && sudo apt-get install tmux 運行:tmux 進入tmux後,你就會至少有一個分頁,而且不會因為Terminal關閘而中斷 用法一: 建立兩個分頁,並切換 增加分頁: 先按 “Ctrl + b” (前置鍵),再按”c” (create) 切換分頁: 在多於一個分頁的情況下,先按 “Ctrl + b” (前置鍵),再按”n” (next) 用法二: 同一個分頁中,建立左右並排的窗口 增加水平窗口: 先按 “Ctrl + b” (前置鍵),再按 “ (雙引號) 切換窗口: 在多於一個窗口的情況下,先按 “Ctrl + b” (前置鍵),再按方向鍵左或右 用法三: 回到前一個tmux session中 因為不小必關閉了terminal,又或是remote ssh中,ssh斷線後,需要回到之前的工作狀態 未進入tmux 的狀態下:tmux attach 要留意tmux 可以有很多個session,要去到指定的session,就要為session命名。但這個不是筆者常用的情境,原本多個分頁已經很夠用,還要多個session,會很混亂。但不排除它在某些情況下有特別用途,有興趣的朋友可以自行挖挖看。 進階: 回頭看過去的terminal screen output 在現代的Terminal中,原本按滑鼠滑輪向上滾,就可以看到過去的資訊,但tmux下反而不行,所以我們需要進入特殊模式 進入Copy Mode: 先按 “Ctrl + b” (前置鍵),再按 [ (開括號中括號) 向上翻頁: 上方向鍵或PageUp 離開Copy Mode: Copy Mode中任何時候按”q” 進階: 複制貼上 進入Copy Mode: 先按 “Ctrl + b” (前置鍵),再按 [ (開括號中括號) 選擇範圍: 移到需要複制的文字起點,“Ctrl + Space” ,然後再移動到結束點,再按”Ctrl + w” 複制 貼上: 離開Copy Mode後,再按”Ctrl + b” ,然後 ] (關括號中括號) 進行貼上 進階: 複制貼上2 某些情況下,我們不允許使用“Ctrl + Space” 或 ”Ctrl + w”,因為它們可能跟系統的組合鍵有衝突,所以需要改為單鍵。 讓tmux使用類似vim的操作模式: echo “set-window-option -g mode-keys vi” >> ~/.tmux.conf 關掉所有使用中的tmux,重開tmux 進入Copy Mode: 先按 “Ctrl + b” (前置鍵),再按 [ (開括號中括號) 選擇範圍: 移到需要複制的文字起點,按單鍵“Space” ,然後再移動到結束點,再按”Enter” 複制 貼上: 離開Copy Mode後,再按”Ctrl + b” ,然後 ] (關括號中括號) 進行貼上 筆者常用的功能就這些,有興趣的朋友可以再深挖一下。 Reference https://tmuxcheatsheet.com/

Ceph Storage 水很深

科技新知
MacauYeah・2024-09-25

筆者不才,早前為大家介紹了一篇關於Ceph Storage的最入門安裝教學。但在後續測試中,發現了一些概念上的問題,需要盡早說明,不然就會像筆者一樣,要砍掉重來很多次。 OSD HDD Ceph Storage的主要功能,就是為Contiainer提供外置儲存空間,它對儲存空間有特定的要求。我們最好在建立ceph cluster(cephadm bootstrap)之前,就為每個node上增加合適的HDD 引述官方說明: OSD (Object Storage Daemons) The device must have no partitions. The device must not have any LVM state. The device must not be mounted. The device must not contain a file system. The device must not contain a Ceph BlueStore OSD. The device must be larger than 5 GB. 簡而言之,大家需要準備新的HDD,不要做任何格式化,讓OS見到HDD但不作任何操作。筆者試過,使用hyper-v VM + hyper-v HDD,也是可以做到的。不過之前筆者於教學中用的 multipass 就沒有這個模擬HDD功能,我們需要使用比較強大的VM作為實驗。 若然HDD是在ceph cluster(cephadm bootstrap)建立之前,就存在的。我們可以經過ceph的網頁介面,或經指令自動加入。 ceph orch apply osd --all-available-devices 若然HDD是在ceph cluster(cephadm bootstrap)建立之後,才加入的。那麼ceph有機會沒法自動發現它,筆者當前的dev版本就出現這問題。我們就需要經指令手動增加 ceph orch daemon add osd NODENAME:/dev/sdb OSD 官方說明文件 https://docs.ceph.com/en/reef/cephadm/services/osd/#cephadm-deploy-osds Reset 在我們做實驗時,若我們想回復到上一個狀態,測試不同的參數差異,Ceph指令並不會即時執行。例如前一句的add osd,想倒回來自行刪掉一些osd,即 ceph orch osd rm OSDID 它就會排隊慢慢做刪除。 但這個過程筆者未有成功過,OSD一直處於繁忙狀態。有機會是因為系統需要保持同步狀態,待成功遷移資料前,什麼都不能動,所以一直都在待刪除的狀態中。 同樣地,當我們想要刪除一些node時,我們使用以下指令 ceph orch host drain NODENAME ceph orch host rm NODENAME 最後也是會卡在刪除OSD的情況 Removing Hosts 官方說明文件 https://docs.ceph.com/en/reef/cephadm/host-management/ Static IP 因為 container 技術,很多都需要固定 IP ,我們做實驗之前,最好先了解你的VM engine如果提供Static ip 。以 hyper-v 建立的 VM ,其實可以同時建立兩張網卡的,一張為預設網卡,用於連網用,另一張則設定為內部網絡。在安裝 ceph 時,經 cephadm bootstrap 所引用的IP則設定為內部網絡的IP。之後基本上使用任何一張網卡的 ip ,也可以訪問到cephadm的網頁介面。如果不是在一開始的階段上準備Static IP ,我們又會在重設/解綁cluster時,同樣因為機器繁忙而卡在不上不下的狀況。

純文字圖案 | 懶人出圖工具

科技新知
MacauYeah・2024-09-20

早前,筆者就介紹過 Markdown / mdbook 等說明檔編寫工具,也分享過用於制於遊戲攻略時,如何加上插圖的情況。那怕是教學、說明、遊戲攻略,使用圖表的方式表達,的確有助於讀者理解。 在Markdown的技術上,圖文並茂是可以的,只是不太方便而已。以制作及修改的成本來講,出【圖】可能都不算最難,更麻煩的是管理。 怎樣教對?點開圖檔整個閱讀?。怎樣搜尋圖片,可以加附註嗎?更新後名字該怎樣取? 老實講,如果可以,有些【圖】,直接經文字轉譯成圖表就最好。 mermaid Markdown 轉成圖,其實坊間早就有一些免費的工具,筆者選擇了 Github 也預設支援的 mermaid 。廢話不多說,直接送出 web 版的編輯工具。 https://mermaid.live/ mermaid 官網 使用它的好處 Github markdown 直接支援,mdbook經插件也可以使用。 易於編寫,也易於閱讀 有支援IT其他範疇的圖表,例如ER,State。 有支援更多其他範疇的圖表,例如gantt,mindmap。 使用它的問題 不支援手動調整位置,全部靠自動調整 ascii chart 若想要更多的位置掌控,其實我們可以回到過去BBS的年代,用文字方塊來砌圖。這個方法很有局限,但也不是完全不能用。 廢話不多說,直接送出 web 版的編輯工具。 https://asciiflow.com/#/ https://kirilllive.github.io/ASCII_Art_Paint/ascii_paint.html 可以選擇中文字符 使用它的好處 任意手繪圖表 使用它的問題 使用中文等字元,還要考慮是否等寛字型問題。 修改文字長度,邊界要重畫。 筆者有試過用來制作遊戲簡略地圖說明,這是比不斷截圖來要得更直觀。但限制就是不要在圖中加入文字,加入英數等符號就算了,再於其他地方加以解釋。如果我們必需在圖中使文字,我們就要控制輸出字型為等寛字型,例如使用【細明體】,就無問題了。不然就要全部使用中文字元(全型符號),不要混合英數。 .-=. .:*%%+.:. :#%%%%%+#. :#%%%%%%%%%%*. .*%%%%%%%%%%%%%%%*+-.. .-%%%%%%%%%%%%%%%%%%%%%%*:. .-%%%%%%%%%%%%%%%%%%%%%%%%%%-. .=%%%%%%*=-::::::::::::::-=#%%#: :#%%*-:::::::::::::::::::::::-*%+. .*#=-:::::::::::--=========-::::-#+. .=#==-::::-=*%%%%%%%%%%%%%%%%%%=::-#-. .#*==--#%%%%%%%%%%%%%%%#..#%%%%%+::**. :#+==#%%%=.+%%%%%%%%%#-:**::+%%%#-:=--*= .-+#*=+%%#.=%+.+%%%%%%%#--====*%%%#-::::-*. :*====+%%%%#.-%%%%%%%%%%%%-:+%%%%%*::::-*: .*=====#%%%%%%%%%####%%%%%%%%%%%%*::-%+. .:##==#%%%%+-:::::::::::--===-::::##. .##====-::::::::::::=#-:::::::-##. .*%*====---==+**#*+=::::::::+%+. .-#%#+====-:-----::::::-+%%#:. :#%%%%#*+===---+*%%%%%*. ..:=#%%%%%%%%%%%%%+:. ..::---:...

感謝賢者模型工作室 - 修復那些年還沒建好就斷樁的模型

手機‧電玩
MacauYeah・2024-09-13

不知道80、90後的朋友們,今年年初有沒有留意Gundam電影? 一向熱愛Gundam的朋友們,一定知道這套大熱作品Gundam Seed Freedom的出現。那是原本的Seed Destiny的後逐全身劇情,正因如此,玩具商也馬不停蹄地推出新模型。在電影加持的情況下,其實很多老模型,都推出重賣(再販)。筆者也不例外,在看過電影後的,不斷翻老模的格價。 不過,最佔上心頭的,並不是購買欲,而是筆者過去,有一台模型組裝到一半就斷臂的MG脈衝高達(Impluse Gundam)。 上網找模型群友求助,不外乎都是找補件或當殺肉(即係給他人當補件或改件用)。但這次勾起回憶,不如就來個修復吧,死馬當活馬醫,實在救不了,就當殺肉吧。 筆者的情況,是因為模型手臂連結點斷裂,用膠水是無法修復的。經一位網友的指點,這必需靠打樁修復。 打樁,基本上有三要素:手鑽(連鑽頭)、銅棒、萬能膠。萬能膠很易可以解決,在本澳各大超市及文具店均有售。手鑽(連鑽頭)、銅棒,就麻煩一點,五金店的都很大,不適合模型玩具用。當然,有何事,去萬能的淘寶就有售。 在淘寶購物車當下,筆者還是很猶豫。萬一買錯了怎辦?淘寶單價不貴,但運費可很要命。是不是找個懂修復的大師,幫忙看看情況,再由大師下單比較適合。 就是這樣,筆者就不斷在網路上找,到底澳門有沒有模型工作室。一找,還真有一家【賢者模型工作室】,主打噴塗裝備的工作室。當時筆者還厚顏地私信問店家,如果不噴塗只素組的話,有免費位置嗎?店家還很慷慨的回答,是的,素組不收費。收費部份,就是耗材購買、噴塗裝備租用。坐位上,只是租客優先。 所以筆者就在本年的八月份,去【賢者模型工作室】朝聖一下,順便帶著斷臂的脈衝,看看有沒有路過的大師幫忙指點一下。【賢者】就如當初對話中了解的一樣,主打噴塗。筆者雖然對噴塗了解不多,但見在場裝備,一定是專業級。賢者不單是提供上色工具,還解決了很多抽風問題,在家想要一個這樣安全的制作環境,一定價值不菲。因為家庭環境問題,放棄噴塗的朋友們,真的可以去【賢者】現場看一看,應該會有所得著。 說回筆者原本的脈衝高達,剛好當晚店長【賢者】在場,拿起筆者的模型細看了一翻,然後就講了一句:"簡單,我幫你修吧"。筆者原本還以為只是來取經,然後再逐一買工具,想不到熱心的店長,突然出手幫忙,實在喜出望外。 手鑽開孔 上銅樁,最後插入主體中 回家再組裝外肩甲,浴火重生 在整個修復過程中,慷慨的店長用的都是他自己的工具,只是他手中沒有已開封的銅樁,筆者就現場買一份吧,其餘一分錢都沒有收取筆者的。店長不但為筆者修復了模型,還省下了工具錢,還傳授了補樁的要點,筆者實在收穫巨大。想不到,澳門玩具業中,還有這麼良心的店存在,實在值得支持。 若然讀者們跟筆者一樣,因為任何原因,有一些未完成的作品(模型、GK),不妨一齊帶上去【賢者】,看看模友們能不能為你帶來新的方向。店長不一定在店,有需要可以加入他們的交流群,打聽一下店長的駐店時間。交流群也不會有店家的推銷產品,不過群友們會推坑團購,大家要管好自己的心。 【賢者模型工作室】 官方專頁 https://www.facebook.com/insmws/

排版之於文字創作者的重要性

手機‧電玩
MacauYeah・2024-09-05

筆者已經很少再買遊戲相關的書藉,一來因為筆者已漸漸遠離遊戲新作,二來發現某些紙媒的資訊瑕疵越來越多,所以就沒有再看。但因為家裏土地資源問題,筆者需要再斷捨離一些收藏,所以就再拿起舊的遊戲雜誌來重新審閱。 說實在,在過去、現在,很多傳媒的資訊都無法確認正確性,其資訊價值難似證明,但某些紙媒一直存在至今,一定是有些地方,很受市場歡迎。那些地方,大家也不妨去借鑑一下,或者對自己的作品會有幫助。 之前某一期,筆者也表明過,UCG的攻略有著抄襲問題,所以筆者已不再購買他們的新攻略。但重翻他們家的百期特輯,內容的確不錯。 主題的選擇 作為百期特輯,他們會選擇一些回顧的主題。而這些回顧也的確實表現出時間的演變。 排版 現時網絡資訊,品質好的,會配以圖文表示。但因為結構單一,所以新鮮感不足。最差的什至會滿頁廣告,閱讀感很差。但以百期特輯的雜誌式的排序,每頁配圖位置稍有不一,亦有主題轉變,風格相似又不盡相同,真的耐看。 匯整 雖然資訊不能保證百分百正確。但以匯整的角度來說,有一讀的價值。好比ChatGPT,它的資訊也不是完全正確,但它可以省略你在搜尋引擎中四處查看、逐一對比的過程。 對於各位文字創作者,有條件,當然可以嘗試以圖文方式豐富視覺內容。那怕沒有時間挑圖,也可以試試定制不同章節的排版風格,來讓故事/文章變得比較有過程的變化或是段落感。 不過,說到尾,筆者並不是對抄襲問題作出認同,我們應對原創有更多的支持,抄襲只會讓原創更難生存。但某些二次整理,也有它的價值。就像筆者的知識分享一樣,重複以不同的方式,更易理解的角度,去重述一些議題,倒是不會嫌多。

推坑SFC的神作

手機‧電玩
MacauYeah・2024-09-03

因為年紀漸大,筆者玩遊戲的機會越來越少。一方面是因為家庭,一方面則是因為身體大不如前,腦袋開始跟不上3D遊戲的場境,常常不是玩一玩就累了,就是玩一玩就暈了,那怕連手遊都一樣。 所以筆者現在什少會再開新坑試新世代遊戲,反而專注在舊世代中,體驗一下過去的名著。值得一提的是,過去的遊戲體量通常比較少但完整,對於繁忙的生活節奏,是適合的。相比手遊,舊世代的遊戲更無抽蛋要素,更沒有那種玩個空虛的感覺。所以若然大家主機有買會員,趁會籍到期之前,快試一試那些年被你跳過的遊戲,應該有所收獲。 今日筆者就選了NS online會員的SFC舊遊戲,《薩爾達 眾神的三角力量》。只要大家有NSO,應該都可以順利重玩這個遊戲。 向大家推坑這遊戲的原因主要有幾個。 薩爾達 Switch 兩作稱霸天下。過去的作品,很值得一試。 眾神的三角力量是平面遊戲,不會暈,而且 Switch 隨時待機,玩玩停停沒有壓力。 玩後的優點: 那個古早的年代,已經發展出到處鼓勵四處探索的玩法。那怕只是2D平面,都感覺到世界的大。 攻略好找好看。薩爾達在 Switch 中的最後兩,好玩歸好玩,但攻略真的難攪,難以用文字來表達。即使各大出版社如何編制圖文包,還不如直接看影片攻略來得直接。眾神的三角力量,那怕筆者不看圖,筆者也知道攻略制作人想表達的意思和方向。不知道下一步去哪?不用怕,看一看網上攻略就攪掂。 偏向動作遊戲,穿插少量劇情。筆者過去都是接觸以劇情量為主的JRPG,以現在的生活形態,要好好地讀完一個新的JRPG故事真的很難。 沒有壓力。這真的很重要。Switch 兩代,都有一個很麻煩的資源系統。武器、道具、盾,都是會快速消耗的。除非大家對操作很有信心,不然每次戰鬥,那怕打贏了,也會覺得消耗過多,選擇重讀存檔重打。這樣的遊戲玩下去很有壓力,跟魂系遊戲有得比。多磨幾次,人也會累。但SFC 舊作,只有副武器為消耗性。你的劍和盾,可以一直用,副武用光,誰怕誰。 參考連結: 筆者遊玩時看的攻略(https://lasjargon.blogspot.com/2014/12/legend-of-zelda-link-to-past-chapter01.html?m=1),雖然覺得不全,但至少讀得懂 由 本封面圖像可由任天堂處取得。, Fair use, https://zh.wikipedia.org/w/index.php?curid=7663843

為何Python這麼熱門?

科技新知
MacauYeah・2024-08-27

在資料處理、資料科學領域,什麼是最近的AI模型,Python都是做這些事的熱門選擇。對於以前從未用過Python來處理業務的筆者來講,實在不懂為何Python會那麼大熱。不過最近,筆者實戰過後,真心覺得它是提高生產力的重要工具,而且並不限於資料科學上面,一些簡單的腳本操作也是很有優勢的。 筆者前述有討論過 型別對程式語言的重要性,到現時這一刻,筆者都會覺得【型別】是有助於長期的程式開發。而Python這個語言,大部份人都會介紹它是動態語言,可以使用弱型別,然後,就沒有其他講法了。動態弱型別,筆者一直都不認為它的根本上的原因。就像Javascript一樣,它亦發展出類靜態強型別的Typescript版本,而且它亦不因此而被人棄用。所以Python的強大,動態語言並不一最重要的原因,它也可以模疑寫出有規有距的type hinting。 或者用另一個方向問,大家覺得 Excel / SpreadSheet 好用嗎?它們可以很簡易地做出資料計算、篩選。而且可以一邊做,一邊調整公式。例如要大家做一個陣列的總和,大家會想打開一個Javascript,初始化陣列的每個數字,然後寫個For迴圈去計算總和嗎?還是打開 Excel / SpreadSheet,打下一欄或一列的數字,然後叫出Sum函數?筆者一定會選擇後者,不單止因為寫函數比較方便,那怕之後要調整數字,也比較方便。 大家有感受到差異嗎?筆者想表達的是,在操作 Excel / SpreadSheet 我們並不是整個程式重新執行一次,我們是修改完一部份,那上看到結果。但傳統的語言,例如C、Java、那怕是Javascript,我們都難以局部地更新或執行特定某一個區塊。那怕是現在我們有hot reload,但其實我們編寫的思維,都是讓我們完整執行起一個頁面,再人手輸入,看結果。如果我們只想運行某個單一Function函數,我們只能寫test case測試,但寫test case又是一個很大的入門門檻。 但大家如果看看Python,在古早的年代,Python已經有Python shell,那就像是Linux Shell或Window CMD一樣,可以一邊寫腳本,一邊看結果。寫了10行的程式,發現在第10行引用第5行的部份有問題,修正並執行第5行後,就可以回來馬上重跑第10行的語句,就馬上有結果了。第6至9行,因為沒有關聯性,就不需要逐一重新執行,那是多麼的方便阿。道理上,我們若沒有完整執行整個程式,可能還是有一些盲點,開發重要的,需要長期維護的程式,還是要像傳統一樣,有test case,有程式進入點,整個運行。但對於臨時性的操作,看看效果,我們實在無必要寫一個原整程式。 舉個例子,假如我們臨時有需要,要取得某個政府網站的即時數據,例如澳門的停車場資訊,空位的上下限是多少,我們絕對可以用python寫幾行就取得結果,然後順便做個資料運算。我們沒有必要很嚴僅地為考慮不同數據的出現情況,我們什至可以hard code - 硬編碼地計算某個Array的元素。直到突然有一天,這個操作變得恆常化,我們還是有條件把之前的python程式碼,改寫成一個規規矩矩的完整腳本,包括異常處理,函數複用。(其實Javascript在改用 NodeJs 作為引擎後,我們還是可以經過 Node.js REPL,來做互動操作,只是Python Shell出現得更早,也是官方支援的功能。) Python這個臨時操作的便利,對於資訊爆炸的年代來講,實在很幫得上忙。再加上現在除了Python Shell以前,還有Jupter Notebook,讓大家可以在Web頁面上,執行像Python Shell的互動操作,對於修過特定區域的程式碼,就更加方便。這些便利,都是不是因為動態語言來創造的優勢,而是實實在在的Coding Anywhere。

Github flow 沒有提及的發佈 - 佈署 | Release - Deployment

科技新知
MacauYeah・2024-08-23

不知道之前為大家介紹的github flow,大家覺得怎樣?好用嗎?今天,筆者又來講講筆者心中認為它沒有好好給出指引的地方。 我們的信心指數,其實沒有那麼高 在前文中,經過 pull request 、 code review 、 auto test ,道理上,開發者可以做的都已經做過了,然後就是等待發佈 - Release。 對於單純的庫類型的程式碼,筆者認為,的確沒有事可以再做,實務上就是直接找人其他程多員試用最新版本,看看有沒有問題。只要 main / master 上,明確的表示版本號的變更,就差不多等於直接發佈。有需要提供binary版本的,就還需要觸發上載binary的流程,但這個跟 pull request 觸發 auto test 差不多, auto test 成功後就上載。 對於服務類型的程式碼,例如 Web App 等,直接發佈到正式環境還是有些不妥吧?始終會即時影響到業務,我們至少有個測試場,經用戶做實際的業務操作去驗收。但這個時機,應該是在Github flow的什麼時候做? 在原始的git flow中,有一個叫做 develop 的相對穩定分支,僅次於 main 。它是功能開發完成後第一次pull request 的地方,我們可以用這個概念來做自動發佈到測試場。但若在github flow 中加入了這個 develop / uat / staging 分支,其實就等於複雜地回到過去傳統的 git flow中,對好多新手來講難以接受。Github flow 的成功簡化,其實很大依賴著自動化測試。現在的測試用例,並不再限於單元測試。就連整合測試,也可以經Docker等容器化技術去做,只要我們的自動化測試有足夠信心,就可以發佈。但反觀我們的 Web App 例子,我們認為自動化測試難似涵蓋所有情境,也難以開發。所以我們還在有個時間發佈到測試場,進行人工測試。 pull request + 快速迭代 筆者結合自己的經驗,配上國外討論區 Stack overflow 的內容,筆者認為Github flow上進行 pull request 後,就是最好的發佈測試場時機。所以我們需要盡快進行驗收測試,完成後在Git commit上加上Tag,以示通過驗收測試,可以發佈正式環境的版本。 不過這個模式是有一個很重要的前題假設:快速迭代。當我們驗收完成後,盡可能快地發佈到正式環境,不然會阻礙下一個功能的pull request驗收,或是覆蓋了上一個pull request的驗收環境。 用反面的例子來說明,如果我們有很多功能需要驗收,或變化很多,或存在多輪的里程碑開發,我們就不適宜那上述模式。最保險的做法,還是回到傳統的 git flow ,引入 develop / uat / staging 分支。但如果大家還是那麼討厭傳統 git flow,筆者還是有另一個提議。 既不想回到傳統 git flow ,但又需要慬㥀的考慮驗收發佈流程 如果開發的功能變化比較大,需要多方面協調、測試、驗收,經歷多次里程碑後,才有一個對外發佈的版本,大家可以考慮分開 Repository 做開發。例如: v1,v2的 Repository 完全獨立。 v1 是已發佈的版本,有獨立的測試場,任何即時候需要修正,就在v1的 Repository 做 pull request。 v2 則是未發佈版本,亦有獨立的測試場。加入任何新功能後,就在v2的 Repository 做 pull request,用自己專用的測試場做驗收。到 v2 正式發佈後, v1 就封存處理,再開一個 v3 作為下一個大版本的開發。這個模式,那怕在庫類型的程式碼也用得上。 這樣做的好處是 git Repository 和歷史記錄都會獨立,自動發佈的腳本程也會簡單明確一些。壞處則是 v1 v2 難以做功能對比,我們只能靠人腦記著 v1 有沒有什麼後期加入的修正和功能,需要同步移植到 v2 中 (相對的,著是同一個Repository,可以利用merge 功能確保 v1 有的,v2 都己處理,只是必需要很懂處理版本衝突問題。

Swarm Mode 上線番外篇:Ceph

科技新知
MacauYeah・2024-08-20

在預設Docker和K8s的容器主導世界裏面,其實一直都缺少了直觀的儲存空間。當你的程序需要讀寫故定的來源資料,該來源就必需是外部的穩定儲存空間,例如是資料庫、NFS。但資料庫、NFS等,要做到真的正穩定,其實就要走Cluster(叢集)模式,確保它們自己本身不是做成single point of failure (單點故障)的元兇。 坊間,只要付得起錢,其實找個穩定的資料庫或NFS,也是有的。但如果你像筆者一樣,只有一塊或多塊【鐵】,就要試試開源的儲存引擎Ceph Storage。 Ceph Storage,有自己特有的CephFS格式,但也支援NFS https://docs.ceph.com/en/quincy/cephadm/install/。也就是,只要我們有足夠多人力,道理上可以自己用實體機去模擬一個穩定的NFS。 因為只是試裝,筆者暫時只用VM來測試,完整的安裝script,可以在這裏找到。script使用Multipass VM,大家有條件的話,可以使用其他VM引擎來看重複。以下是一些官網上沒有提的重點: Ubuntu 24.04 還未能正式使用。在筆者做POC的當是,Ceph v18 在 Ubuntu 24.04上需要先解決,即使大家使用Curl base下載 binary,也未必能成功。 筆者成功測的版本是 Ubuntu 22.04 + Ceph v17,全使用Ubuntu 發佈的內置版本。但大家也要留意自己的Ubuntu apt 有沒有更新到最新版,過去的 cephadm,引用的container image url也變更。記得更新到v17 的最新版,cephadm 指令才能成功取得image。 在官方說明文件的【Deploying a new Ceph cluster】中的【Adding Hosts】https://docs.ceph.com/en/reef/cephadm/install/#adding-hosts 節章可能有些誤導,大家應該要看 【Host Management】中的【Adding Hosts】 https://docs.ceph.com/en/reef/cephadm/host-management/#cephadm-adding-hosts 在每個節點內,可以直觀地連接地Ceph Dashboard,但若大家需要Port Forword,要注意你的Network Interface,筆者就只能經過預設的IPv4的public ip 進行ssh port forward,不能經過0.0.0.0。 Script 位置 https://github.com/macauyeah/ubuntuPackerImage/blob/main/initCephCluster.sh

Spring Data 關聯型態 02

科技新知
MacauYeah・2024-08-09

Presist and Casecade 前次的文章,講了一些Spring Data最基本的關聯概念,但當要正式儲存或刪除,就有些考慮完整性問題。平常我們在處理資料庫的關聯表格時,也需要面Foreign Key的正確性問題。同樣地,Spring Data也有這方面的考量,但它有提份一個很方便的CascadeType選項,可以簡化一些流程。 假設你只能存取Parent Repo,那你需要在Parent中,加入CascadeType.All。當repo.save(parent)時,它就會順多把所有child的也一併進行Save,你也不需要有Child Repo的存在。 @OneToMany(mappedBy="parent", cascade = CascadeType.All) List children = new ArrayList(); 但在複雜的狀況下,例如你不想在更新parent的情況下,不小心弄到child,特別是經過public web下的API操作,你對web client的資料正確性有存疑,就不要使用CascadeType了。這也是筆者認為在大多數情況下,我們都會把Parent和Child的CRUD分開操作,然後根據需要使用各自的repo save。 如果你一定要用CascadeType.ALL (CascadeType.REMOVE),就要再留意刪除的問題。為什麼?因為刪除 parent,其實指的是某個parent不再存在,但不代表child也要一起刪除,child的parent連結可以變為null,也有重新連結其他parent的可能。 如果大家確定需要共同刪除,就可以用CascadeType.ALL 或 CascadeType.REMOVE。 還有一個新的選擇,orphanRemoval = true,也有類似效果。 @OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE) List children = new ArrayList(); // or @OneToMany(mappedBy="parent", orphanRemoval = true) List children = new ArrayList(); // or @OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE, orphanRemoval = true) List children = new ArrayList(); 筆者測試過,混著用也是可以的。若大家看過其他教程,可能會覺得orphanRemoval = true 和 CascadeType 總是一起出現,但它們其實是分別操作的。單獨使用orphanRemoval = true,有時候則是為了不會出現無主的child,但這不代表parent和child的想要同步更新。 JPA Entity 的生命週期 Spring Data跟傳統的資料庫Selete,Create,Update,Delete SQL 語句有所不同。也就是這個不同,它的CascadeType比資料庫的Cascade Update和Cascade Delete更強大。 Spring Data 預設其實是使用 jakarta.persistence.EntityManager,每個Entity主要分為四個狀態 Transient / New - 不在EntityManager的掌控中 Managed - 在EntityManager的掌控中,將會在下次flush時,變成sql create或update statement Detached - 脫離EntityManager的掌控,不受flush影響 Removed - 在EntityManager的掌控中,將會在下次flush時,變成sql delete statement 在Spring Data / Jpa 以前,我們若要直接操作Hibernate,經常見到persist, remove的寫法 entityManager.persist(entity); entityManager.remove(entity); entityManager.detach(entity); entityManager.merge(entity); 其實persist就是把處於Transient、Removed的entity,改為Managed。而remove就是把Managed改為Removed。detach,merge也類似,就是Managed,Detached之間互換。 EntityManager最強大的是,它可以讓程序員不需要再為Managed狀態下的entity操心,它會自動判別下次flush,應該create還是update,如果完全沒有改動的,連update也不會執行。 (註,flush和commit也有不同,flush就是從java寫到資料庫中,在資料庫commit前,還可以使用rollback放棄。) 而Spring Data,則是進一步簡化,它把persist改為save,remove改為delete,然後自動選擇flush的時機。 CascadeType 在解釋完Entity 的生命週期後,終於可以回到CascadeType了。這裏的CascadeType不是資料庫的Cascade操作,其實它是指EntityManager的狀態操作是否有傳遞關係。亦即是,persist(parent)時,要不要連同child也一起操作? 我們查看 CasecadeType 的原始碼,就可以發現可以被傳遞的操作共有以下這些 PERSIST MERGE REMOVE REFRESH DETACH ALL (以上全部) 這裏的 CasecadeType.PERSIST ,跟資料庫的 Cascade Update 是不一樣的。資料庫裏的 Cascade Update,是指當 Parent 的 Primary Key 有變,對應child的 Foreign Key也一起變。但因為 JPA Entity 的機制, Parent 的 Primary Key 不可以改變,理論上不會發生類似資料庫的 Cascade Update,頂多有 Cascade Delete。 CasecadeType.PERSIST 就像之前述的生命週期解說一樣, 把 parent和 child 一起拉到受管理的狀態。 註: CascadeType.REMOVE有點尷尬,似乎有更特別的使用規範。筆者測試過,在某些情況下,CascadeType.REMOVE無法處理ForeignKey問題,又或者是,刪除的順序不對。詳見 spring boot data deletion Reference entity-lifecycle-model spring boot data deletion