最近正好汰換下來一台 Pixel 4a,正在想辦法將他的剩餘價值發揮到最大化。前篇 把舊的 Pixel 改造成無限照片上傳機 已經發揮了一些價值,但是不夠!這次我要用這個手機來取代掉我長久以來租的雲端虛擬機,把月租費省下來。
要用手機來取代雲端虛擬機器,其實還是有一些困難,首先家裡並沒有固定 IP,也就是說用手機來當作伺服器的話沒辦法在外面存取,如果有架設網站的需求的話那這肯定是行不通。
不過我的情況剛好不需要固定 IP,我要的其實是一個開發機兼永不停機的伺服器讓我可以在上面跑一些定時的工作或者是跑一些需要長時間運作的程式。
那接下來就是要來思考我需要什麼,主流在安卓設備上面跑 Unix Server 有兩種方式,一種是使用 Termux[1],另一種是直接安裝完整的系統在手機上[2]。
Termux 的好處是安裝十分容易也不需要 root,壞處是他雖然是 Unix-like 的系統,但是算是閹割版,可能有少數一些指令或者 package 是沒辦法使用的;反之,直接安裝完整的系統那當然可以獲得最像是 Ubuntu 的體驗,但相對來講比較難裝較複雜,可能也免不了需要取得 root。
考量到基本上我只需要有可以開發及部屬 Python, node.js, C++ 的應用程式,所以我選擇使用 Termux 就好了。
我的需求可以簡單列出:
Termux 真的十分強大,大部分的指令都跟在使用 Ubuntu 一樣簡單,但我還是遇到了一些坑,特此紀錄一下。
Termux 安裝好後,還需要安裝 openssh 等東西才能使我可以從外部 ssh 進去。
1 | $ pkg install openssh |
sshd
也可以用 screen
等等的指令執行背景,就不會占用前景。
另外也可以用 ssh key 的方式登入,就不需要每次打密碼。
而 $PREFIX
是 Termux 的根目錄,一般常見的 /usr
, /etc
等等路徑在 Termux 底下就會被對應至 $PREFIX/usr
, $PREFIX/etc
1 | # python |
三行指令就安裝完絕大部分需要的工具。
另外開發 C++ 時經常需要 libboost,很可惜沒有辦法直接透過 pkg install
,但可以用原始碼編譯:
1 | $ wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.gz |
安裝完後就可以用 cmake 找到 libboost.
vscode 就相當多坑了…首先,vscode 無法透過 Remote-SSH 套件連線到 Termux,因為缺少必要的一些東西 (libstdc++, glibc 是閹割版)[3][4]。所以可以改安裝 code-server。
code-server 是一個 Web 版的 vscode,從介面到使用方式都與原生 vscode 十分相似,所以用這個也是完全可以接受的解決方式。
但要安裝 code-server 十分之困難,我後來找的一個全是韓文的影片才順利安裝成功:
1 | $ pkg install nodejs-lts python yarn binutils |
然後就可以打開瀏覽器 PRIVATE_IP:8080
即可看見熟悉的 vscode 畫面。
但是事情沒有這麼簡單…這 vscode 壞掉的地方可不少,他 built-in Terminal 打不開,搜尋功能無法使用,Source Control 裝死….
經過一系列研究及嘗試後[5],終於把全部都解決了…
1 | ### bulit-in Terminal cannot open |
這些改完以後,記得重啟 code-server。
htop 在 non-root Termux 是半殘的,看不到 CPU loading,這是由於 Android 系統級別的設定,讓一般應用程式 (Termux 也算應用程式) 無法存取到 /proc
。
所以可以改安裝 pkg install neofetch
,他可以正確展示 CPU 使用量 (如本文首圖),不知怎辦到的 哈哈。
在安裝好所有東西以後,隔天我就發現無法 ssh 到手機上了,一看才發現被系統殺掉了。現在比較新版本的 Android OS (12+) 都有嚴格的執行緒限制,應該是 32 左右,這數字太小了會導致 Termux 經常被殺掉。
所以需要透過 adb 去把這個東西關掉[6]:
1 | # For Android 12L & Android 13: |
Server 通常都要是 UTC 時間會比較方便,結果這其實不需要在 Termux 內用指令調整,只需要去手機 設定>時間>選擇時區 即可。
由於手機是透過 WiFi 連上家用網路,private IP 是透過 DHCP 分配,但如果 IP 經常改變很不方便。
可以通過家用 router 去設定 mac address 綁定分配的 private IP 來避免 IP 跑掉。
(ASUS router 有這功能[7],不確定其他廠牌有沒有)
我原先在用的是 Nanode 1 GB (1 shared CPU + 1GB RAM)。記憶體只有 1GB 有時候會 Out of Memory 很不開心,效能方面我倒是覺得堪用。價格方面則是一個月 $5 USD,是 Linode 提供的虛擬機中最便宜的選擇[9]。
做了一些簡單的效能測試,看看 Nanode 1 GB v.s. Pixel 4a 是誰贏?
1 | [Task1. Compile libboost v1.81.0] |
Task1 測試的所有 CPU 都滿載時的效能,很合理的 Pixel4a 有多顆 CPU 自然會贏過 Nanode;
Task2 是用 Python 寫的 is_prime
script[10] ,用於測試單一 CPU 的性能,就可以發現其實手機的 CPU 還是偏爛,有差到 30%
這邊所謂 ”高畫質” (Storage Saver) 其實是有壓縮的,目前只有 Pixel 1 可以免費 ”原始畫質” (Original) 上傳,但對我而言有免費高畫質就很好了,直接省下訂閱 Google One 的費用。
如何達成這個任務呢?基本上的流程會是以下三個步驟:
但在這裡,需要注意的是,新手機不要開啟 Google Photos 備份功能,以免佔用雲端空間。
接下來,我們需要尋找一個雲端相片同步服務來當作中繼站,讓新手機拍攝的照片同步至舊手機的暫存空間,以利後續的備份。
在我的實作中,我選擇了 Mega 和 MegaSync。
上面流程圖簡單的展示了整個流程,接下來就是把需要的東西準備好:
Mega 是一個雲端儲存服務,提供免費的 50GB 儲存空間[2],註冊且下載至新手機以後以後,只要開啟 Camera Upload [3] 功能即可。空間方面也不用擔心耗盡,一方面是他有 50 GB,另一方面是我只是要將它當作一個暫存中繼站,只要後續備份完成,這邊的照片即可清空。
而 MegaSync 是一個第三方 app,功能類似於 rsync
,可以將 Mega 上面的指定資料夾下的檔案同步至手機地端,所以只要設定好要同步 Camera Upload 資料夾,就可以利用這個機制去下載我新手機所拍攝的照片到舊手機上。
至此,新手機拍攝的相片已經同步至舊手機,這時候只需要在舊手機的 Google Photos 開啟 “高畫質” 備份選項,一切就大功告成。
另外為了維持整個流程穩定,最好要把
才不會被系統限制導致無法在背景執行這些程式。
總結來說,透過這樣的改造,我們可以輕鬆地享有 Google Photos 的高畫質無限免費上傳功能,並且也能賦予舊裝置一個新的功能而非擺在抽屜閒置。只要按照以上的步驟進行操作,就可以輕鬆地完成一台無限照片上傳機的改造。
隨著 C++ 的發展,現在 modern C++ 如 C++14, 17 等等,新增了更多方式讓開發者在編譯時期完成更多事情,比如說更方便的if contexpr
等等功能。而這其實也是被鼓勵的,因為能在編譯時期就處理完的話就可以讓 runtime 執行得更快!
但當大量使用 template 或引用更多的 library 也讓 compiler 的工作越來越多,而如果每改幾行就要等待編譯幾分鐘才能知道執行結果的話,對於一天要編譯數百次的開發者而言實在是太浪費生命了。
本篇文章就要來探討各種加速 C++ Compile Time 的方式,大部分的方法都是 Stack Overflow 搜刮來,然後由我自行實測。測試環境如下:
引入 ccache 絕對是效益最高的加速方式,完全不用改程式就可以減少大量的編譯時間。ccache 是一個全域的 compiler cache,藉由快取編譯的中繼檔來節省重新編譯的時間。安裝好以後只要在 CMakeLists.txt
中加入:
1 | # CMakeLists.txt |
即可使用ccache
,如果專案沒使用 build tools 的話,則是直接在gcc
指令前加上ccache
1 | # before |
使用 ccache 之後整體編譯速度大約可以提升兩倍以上,十分讚!
C++ 的 #include
關鍵字其實就是複製貼上,所以當你在 A.h include 了 B.h,在預處理階段編譯器會把 B.h 內容複製到 A.h,而如果不幸的 B.h 又 include 一堆檔案,那也會通通展開。所以如果引用太多檔案,除了會造成預處理之後檔案肥大以外,也會造成檔案之間相依性混亂,間接導致每次編譯要重新編譯不必要的檔案。
除了將沒用的 include 清乾淨以外,還可以更激進的避免在 header include 東西,那就是利用 forward declaration。
試想以上情境,當你變更 A.h 時,A B C 都必須重新編譯,因為內容改變了,但實際上 C 並未使用到 A,其實應該可以避免重新編譯 C。
由於 C 會重新編譯是因為 B.h 內容改變了,而 B.h 內容改變的原因則是因為 A.h 更新了。這時候可以檢視為甚麼 B.h 需要引用 A.h,看看是否可以避免引用。
1 | /// B.h |
以上是常見的使用情境,B 存了一個 class A 的參考A& a
。
我們可以改寫成這樣,將 include 移至 B.cpp 實作檔中。這是因為A&
, A*
等這類東西的大小是固定的,所以在定義時不需要知道實際 class A 的大小,只需先告知 compiler 有這個 class 即可。
1 | /// B.h |
如此一來,當你變更 A.h 時,B.h 內容並不會改變,也就不會觸發 C 需要重新編譯拉,可喜可賀~
在大量使用這個技巧以後,我所測試的專案進步幅度也是非常明顯,更動 A.h 原本會牽動 54 個檔案需要重編譯,改完以後則只會牽動 29 個檔案,自然編譯速度也就變快了。
1 | # before use fwd v.s. after use fwd |
Unity build 又稱 Jumbo build, Mega build,其原理是透過將*.cpp
彙整成一個all.cpp
再一起執行編譯,這樣就是省下 N 個檔案的編譯時間 (具體而言是省下如 template 展開等原本每個 Translate Unit 都要做的事情)。
CMake v3.16 開始就支援 Unity Build 的設定,他支援將 batch size 個檔案先匯總成all_x.cpp
之後再進行編譯。
不過這方法會遇到一些問題,由於這方法之原理說白了就是cat *.cpp > all.cpp
如此暴力,如果專案本身常常使用全域變數的話,這會很容易導致 ODR (One definition rule) 錯誤。所以也有可能不容易引入 Unity Build。
這個技巧我認為也是 CP 值十分之高的方法,幾乎不用改程式 (如果專案用太多全域變數就要改很多😅) 卻可以獲得大幅的進步。我測試的結果如下,可以看到無論是 incremental build 還是 clean build 都取得 50% 以上的進步。
1 | # w/o unity build v.s. with unity build (batch_size=8) |
編譯的最後階段是 linking,這部分可以替換成比較厲害的 linker,市面上目前有三種較有名的 linker
要替換使用 linker 只需要在 compile flag 加上fuse-ld=<linker_name>
即可。詳細可參考 gcc document。而我實測不同 linker 表現如下,
1 | # rebuild using single thread, unit in second |
使用更強的 linker 雖然使 linking time 進步許多,但對整個專案的 compile time 而言其實佔比不是很大,相較於前面幾個章節算是進步較小的技巧。(但 CP 值也是很高,只要改一個 compile flag)
我們可以透過 gcc flag -ftime-report
來剖析編譯各個階段的耗時,然後針對各個耗時大的改善。
我測試的專案中,有一個 auto-generate 的unordered_map
,該檔案動輒數萬行,每次編譯該檔案都會成為瓶頸。從-ftime-report
得知編譯該檔案耗時最大的部分是 var-tracking,var-tracking 是讓 debug info 有更多資訊的功能,但當專案中有巨大的變數時,這會讓 compiling 速度大幅變慢。
在對我那個數萬行的unordered_map
檔案拿掉 var-tracking 之後 (針對該檔案加上一個-fno-var-tracking
flag) 結果如下,
1 | # gcc -ftime-report auto_gen.cpp |
結果是從原本耗時 134 秒降低至耗時 55 秒,減少超過 50% 的時間。這也使得該檔案不會再是整個專案的瓶頸。
本文嘗試了許多技巧來加速編譯所需的時間,總結各點如下列:
ccache
[big improvement]在爬文時網友提及 pre-compiled headers 以及 explicit (extern) template 也對減少編譯時間有幫助,但實測並未有顯著差異,故本文未提及,也許實際上是有用只是剛好不適用於我的環境之類的。
/home
家目錄放置於 Share storage 中,然後再掛載至所有工作站的機器上,這樣就可以讓使用者無論是登入哪一台工作站機器,都可以保持同樣的家目錄環境,十分方便。除了前言提到的使用情境以外,其實常見的使用方式也有像是拿來做為平行運算寫入的位置,或者用於存放大量資料集(dataset)給需要的人方便讀取(像是之前 CML 就是這樣用)。而諸如此類的應用其實就很考驗 Share storage 的讀寫性能,若性能不佳可能常常會壞掉,導致所有有掛載的機器都會陷入無法存取的窘境。
而本篇目的就是想要測測看市面上常見的 share storage 解決方案以及他們各自的寫入性能,做個大PK。底下列舉的就是我打算要測試的幾種 share storage (由於純讀取通常效能都很好,所以這邊只測寫入效能)
其中前兩個(Gluster, Ceph)需要自建,後三個(GCP, AWS, IBM)則是直接用雲端解決方案。
測試的方式我就用原始又有效的 dd
指令來試啦,基本上腳本長這樣:
1 |
|
這腳本有幾個參數可以調整:
bs
- block size 指單一寫入區塊的大小count
- 寫幾個區塊of
- 寫到哪裡,/mnt/
就是我們要測試的 share storage 的目錄bs*count 就是總寫入大小,我這邊就統一使用 bs=10MB, count=100, 總計寫入 1GB。
而這個腳本執行的結果會像是這樣:
1 | $ bash test.sh |
但這樣還不夠,要測試到極限的話,需要平行化同時執行一堆這個腳本,寫爆 share storage!
100x10MB, 10 jobs | 43 | 12 | 67 | 76 | 13 |
---|---|---|---|---|---|
100x10MB, 20 jobs | 12 | 6 | 29 | 66 | 6 |
100x10MB, 40 jobs | 9 | 6 | 21 | 33 | 2 |
Gluster | Ceph | GCP | IBM | AWS | |
---|---|---|---|---|---|
100x10MB, 10 jobs | 43.6 | 12.1 | 67.2 | 76.5 | 13.6 |
100x10MB, 20 jobs | 12.0 | 6.2 | 29.3 | 66.1 | 6.1 |
100x10MB, 40 jobs | 9.0 | 6.0 | 21.1 | 33.9 | 2.8 |
▲ 不同 storage 寫入效能比較[1][2]
結果如上圖所示,自建的系統其實表現都遜於雲端方案(尤其是同時平行寫入很多時),這不排除是因為我安裝 Gluster / Ceph 時基本上都是用預設的設定,沒有特別研究怎樣最佳化😱;而 AWS EFS 表現奇差,這有原因,底下詳述…。除此之外又穩又快的就是 GCP Filestore 以及 IBM File Storage 了,其中 IBM File Storage 在同時平行寫入很多時表現比較穩定,所以在這邊是 IBM 表現最佳🏆。
AWS EFS
AWS 的 EFS 的讀寫流量是有做限制的,基本上就是如果存越多資料在 EFS 裡面,則讀寫流量越高 (bursting mode),這也造成基礎流量超低,如果用的儲存空間只有 100G,則讀寫流量大概落在 5 MB/s,每增加 100GB 的空間用量會增加 5 MB/s 的速度[3],是個很妙的設計。在我這次的測試中,由於 EFS 基本上是空的,所以讀寫效能自然低下。
除此之外,CouchDB 同時也主打所謂的 muti-master cluster 架構,可以輕易地設定多個 CouchDB instances 來達到 HA 的目的,確保服務不會因為伺服器掛掉而無法存取。
而本篇就是在記錄如何透過 Docker Swarm 來佈署跨機器的三個 CouchDB 並且將之設定為 cluster mode.
在開始之前,由於我是打算要用 docker swarm 做跨機佈署,所以首先要先準備好環境:
如何將 docker 設定成 swarm mode 可以參考文件,基本上就只要:
1 | # init swarm |
設定好之後可以用 docker node ls
確認一下,結果會類似如下:
1 | $ docker node ls |
我們要使用的會是官方的 docker image — couchdb:3.1.1
要使用其實不難,這邊可以示範一下在本機佈署 single node 的方式,docker-compose.yml
1 | version: "3.8" |
可以透過 docker compose 來嘗試執行這個檔案:
1 | $ docker-compose up -d |
然後就可以訪問 CouchDB 內建的管理介面: http://<IP>:5984/_utils/
,接下來到 setup 頁面依照指示設定 single node,即可。
至此就完成 Single Node CouchDB 的安裝,可喜可賀。
剛剛嘗試了一鍵佈署 single node 的 CouchDB,那接下來就來嘗試主角吧 — Cluster Mode
CouchDB 的 cluster mode 設定比起 single node 來的複雜非常多,而且存在許多坑 (都不會有 error log 的坑),我這邊就是紀錄我摸索無數夜晚得出的結果😵
首先我先列出要設定 cluster mode 必須要滿足的條件:
COUCHDB_USER
, COUCHDB_PASSWORD
, COUCHDB_SECRET
, ERL_FLAGS
NODENAME
來互相溝通為了要保證大家的 Config 一致,這邊我要用事先準備好的 config.ini
,而非透過 yml 的 environment 傳參數,這個方法也是官方建議的方法[1]:
The best way to provide configuration to the
%%REPO%%
image is to provide a custom ini file to CouchDB, preferably stored in the/opt/couchdb/etc/local.d/
directory. There are many ways to provide this file to the container (via short Dockerfile with FROM + COPY, via Docker Configs, via runtime bind-mount, etc), the details of which are left as an exercise for the reader.
那接下來就來準備 config.ini
:
1 | [admins] |
這邊就有一個坑,就是 admin 的 password 必須要是 hash 版本的,如果這邊是 plain text 的話,在啟動時 CouchDB 會自動做 hash,然後就會導致三台 CouchDB 的 password 不一致 (同樣的密碼 hash 的結果會不一樣,相關文章: Timing Attack in String Compare );密碼不一致就會出現 unable to sync admin passwords
錯誤。
那關於要如何獲取 hash 過的密碼,官方是推薦透過建立一個 dummy 的 single node,然後去看他 hash 出來的密碼長怎樣,再 copy 過來 (好蠢…)
這邊提供另一個方法,如這篇文章[2]所說,可以透過 python script 產生 hashed password:
1 | $ PASS="admin123" SALT="8a3bfe04b1f4294d89d9e9d250fce77a" ITER=10 \ |
官方說了三種方式提供 ini 檔:
所以其實只剩下 Docker Config 較適當。
docker config 設定方式如下,
1 | services: |
我們將事先準備好的 config.ini
透過 docker config 掛載至所有 CouchDB 的 local.d
資料夾。
這裡有另一個坑,有可能會出現 CrashLoopBackOff 的狀況,我嘗試發現根本沒辦法掛載到 /opt/couchdb/
底下的任何目錄,大概是 bug 吧,這邊有相關 GitHub issue[3]。
為了繞過這個不能 mount 的問題,必須要覆寫 entrypoint,先把 config file copy 到適當的位置再執行原本的 entrypoint,所以會改成這樣:
1 | services: |
這樣改意思是先把 config 掛載至別的地方,然後在執行 entrypoint 時先 copy 至正確位置之後再執行原本的指令。
至此我們已經可以撰寫出完整的 docker-swarm.yml
1 | version: "3.8" |
這邊我用 global mode
是因為我希望每一台機器恰好只有一個 CouchDB。(上面的 yml 我也沒有掛 volume 所以資料會在 container 不見時一起消失)。
NODENAME: "{{.Service.Name}}.{{.Task.Slot}}.{{.Task.ID}}"
則是因為透過 docker swarm 佈署時,他的命名規則就是長這樣,在 docker 裡面是可以透過這個 nodename ping 到別台的。要查詢 docker container name 的話只要 docker ps
就可以看到。
接下來就直接佈署:
1 | $ docker stack deploy -c docker-swarm.yaml test |
每個 node 都起來之後就可以去做最後的設定,CouchDB 設定 Cluster 的方式是透過 admin http api 去把其他 CouchDB 加進某台。
1 | $ curl -X PUT "http://admin:admin123@<IP>:5984/_node/_local/_nodes/couchdb@test_couchdb.2v2lb55cyes0rf3tbtqe2zp9x.strqjl8lsdm58tozn59mp8du7" -d {} |
都加好之後,可以透過 /_membership
檢查是否正確:
1 | $ curl "http://admin:admin123@<IP>:5984/_membership" |
這樣就設定完成啦🎉 (記得再去管理介面 verifyinstall 檢查一次)
設定好 cluster 之後就要來驗證 HA 是否正常,這邊測試的方法會是先在某台 CouchDB 新增資料,理論上其他台也會可以存取這筆資料:
先建立一個新 database 以及一個新 document:
1 | $ curl -X PUT "http://admin:admin123@<server01>:5984/mydatabase" |
接下來這個 document 會自動 replicated 到其他兩台 CouchDB,可以透過分別 curl 每一台來驗證是否有一樣的 document:
1 | $ curl "http://admin:admin123@<server01>:5984/mydatabase/01" |
這樣即使任意機器掛掉,整個系統都還是可以維持運作。
(實務上再去疊一層 Load Balancer 讓 Http endpoint 統一會更方便)
設定 single node 很簡單,但設定 cluster mode 頗複雜,我個人覺得 error log 沒有非常完整,很多各式各樣的坑都會直接死掉根本不會有任何 log,很崩潰…😱。
而這麼小的日食觀測眼鏡也不太容易直接套在單眼相機鏡頭前,所以我最後就索性決定這次只使用我的手機拍攝,順便來實測看看用手機拍攝日食到底可不可行?
一直以來天文攝影都是件昂貴的興趣,我個人其實除了以前在天文社能夠接觸到厲害的器材之外,畢業之後其實自己也沒有自行購買單眼或望遠鏡的裝備,簡單來說我器材都是蹭飯借來的…🤣
手機鏡頭跟單眼鏡頭其實最大的差別就是鏡頭大小,這直接影響了天體的進光亮,當然也直接影響訊躁比,這個差別在一般的天體攝影或星野攝影都是手機鏡頭巨大的弱勢,但日食則是例外,我們永遠不會擔心拍攝太陽的進光亮不足,只擔心進光亮太足燒了我的鏡頭🔥。
雖然手機鏡頭小對拍攝日食影響不大,但要用手機拍攝太陽還是必須解決許多問題,比如說:
關於這幾個問題,我就分享我自己的做法:
太陽是個世界亮的東西,不減光肯定什麼鬼都拍不到。最簡單的減光方式就是把路上會發的日食觀測眼鏡剪下來貼在手機鏡頭前面,就可以了~🎉
我是貼在手機殼上面,這樣也方便拆卸。
手機相機軟體通常都沒有提供手動控制快門時間、對焦、ISO 或其他參數,這部分也是我無法解決的部分,我的相機是不提供手動對焦的,所以我只能想辦法放大一點,然後跟智障一樣一直狂按太陽祈禱他會心血來潮對到焦…。我大概拍攝了 400 來張,真的夠清晰的不到 100 張,慘慘慘。
另外假設手機有支援儲存 RAW 檔的話一定要存 RAW 檔!RAW 檔可以保留照片更多資訊及細節,在後面要做影像的細部調整或拉曲線等等都較不易失真。
太陽其實只有半度的視角,不放大根本只占照片中小小一顆的,如上圖右一。要透過手機提高有效焦長,一是可以購買額外手機望遠配件 (感覺配件店不少類似的,但品質未知。);二是目前不少手機有長焦鏡可以做到光學或數位變焦 (像是 iPhone 可以 2x 光學變焦) 也是可以湊合著用。而我是直接用 Pixel 4 數位變焦到八倍,順便測測他宣稱的 super res zoom 效果。
剛剛上面提到的幾點都是可以預想到並且先預練過,但真的上場時還是遇到其他突發狀況,像是我開拍不到半小時手機就直接熱到當機,然後直接關機掰掰….。只好趕緊在旁對手機搧風吹氣待它降溫再強行開機,旁邊的人一定覺得我是怪人😂
熱到當機這狀況直到食分夠大時,氣溫明顯下降後就改善許多哈哈。
就我這次的拍攝結果來說,我覺得其實目前的手機很多應該有辦法拍攝到跟本篇同等程度的照片,雖然離單眼或真正專業的器材所拍出來的還是有差距,但對於一日天文迷或沒錢的天文迷,其實用自己的手機也是有辦法記錄下這些特殊天象的。我個人覺得最難克服的會是手動對焦及熱到當機的問題,這兩者能解決就可以很輕鬆的拍攝了。
日食結束後上 PTT 發現有這篇文 [問卦] 手機拍太陽!?!? ,本文應該算能夠解答這問卦~
日環食跟偏食完全是不同的體驗,在食既到復圓之間,周遭氣溫、環境光變化十分明顯,感覺就像是突然變黃昏那樣暗但天空卻沒有黃昏的橘色,很難形容的光線;溫度也突然變涼爽。且湖面上閃耀著暗暗的光(?),整個世界都感覺很不自然。🤯
環食那刻全場歡呼,我也親眼見證了那神奇的一刻,絕對比我之前看過的任何天象都神奇,不枉費我曬的 6 小時的太陽。
]]>本文旨在在 Google Cloud Platform 上建立由 Slurm[1] 管理的運算叢集,並且也建立一個 NFS 自動掛載在所有 compute nodes 上面供大家讀取及寫入。
在開始之前要先釐清這個 cluster 需要的東西,基本上是下列:
整體圖大概長這樣,基本上參考 google 的架構[2],只是加上一個 NFS
首先先來建立一個儲存空間來當作 NFS ,用於掛載在所有 Nodes 上,這樣才可以在任何地方存取同樣的資料,我這邊選用 Google Filestore 因為他的 I/O 會比一般 Google 的 pd-standard 或 pd-ssd 來的好[3]
建立這個就不太需要介紹了,就跑去 GCP console 案案案就好了,其中一個要注意的是 authorized VPC network 如果沒特別需要,可以選 default 會比較簡單。
接下來要來佈屬 Slurm controller, compute nodes, client VM 到 GCP 上,這邊其實已經有整個模板了,github 連結。
這個可以直接用 gcloud
佈署,但在此之前需要先改改 config ,主要要改的是 slurm-cluster.yaml
asia-east1-b
default
,不填的話會自動建立 {cluster_name}-network
的 VPC其他就參考以下吧
1 | imports: |
另外一個要改的是 scripts/startup.sh
,這個 script 是 VM 啟動會執行的,由於我們用到 NFS 所以要安裝 nfs package:
1 | # scripts/startup.sh |
我個人認為最大的雷就是 filestore 跟 slurm cluster 必須在同一個 VPC 才能掛載,然後這個 slurm template 不指定 vpc 他會幫你建一個 (不是用 default),所以我一開始搞一直不同 vpc 掛不起來…
設定通通解決後接下來就是佈署上 GCP 了,基本上透過 gcloud 就可以了:
這邊可以 clone 我改過的設定檔 (跟上面說的設定一樣),記得要填 nfs IP
1 | $ git clone https://github.com/SSARCandy/slurm-gcp.git |
接下來要等一下大概五分鐘,因為要建立一個 compute node 的 image,之後要自動擴展 compute node 時使用。
待一切就序之後就可以登入試試,可以看到 slurm_nfs
也有掛載在上面:
1 | $ gcloud compute ssh cloud-slurm-login0 --zone=asia-east1-b |
利用 sinfo
可以查看 slurm cluster status,有一百台等著被使用。這一百台都是假的,要等到有人真的發 job 才會建立,然後閒置太久就會被關掉,是個很省錢的方式呢~
1 | $ sinfo |
試試看發很多個會寫檔到 NFS 的 job,先建立個 slurm_filewriter.sh
,這個檔案會執行寫入 1GB 的資料到 NFS 裡。
1 |
|
然後透過 sbatch 發 jobs,關於 slurm 用法可參考這個小抄
1 | # send 5 parallel jobs that write file to NFS |
至此整個 slurm cluster 就佈署到 GCP 上了,可以開始開心使用啦~
這樣的優點包含可以無上限擴充運算資源,再也不用等待!
NFS I/O 根據我的測試也是完勝自架的分散式儲存系統!
壞處則是可能也許會有些貴…(?)
雜談
噫!好了!我畢業了!
]]>我這次要搬移的是我原本實作在 theme 裡面的功能,就是可以把內文圖片放大的功能,非常像 Medium 網站上按圖片會有的效果。效果如下:
原本我是直接在 theme 中使用 @nishanths 的 zoom.js,直接引用他的 sciprt,並自己註冊一個 Hexo tag:
1 | <!-- in theme layout file include external resource --> |
1 | /** |
透過 hexo.extend.tag.register
可以註冊新的 tag 語法,可以直接在文章 markdown 中使用,這個 tag 本身把 {% zoom %}
轉換成完整的 html 格式,並且由於已經在前端引用 zoom.js
library,所以就可以正常運作。
原本的作法是直接在 layout 中引用 zoom.{js,css}
library,這當然可行,但當要把這功能模組化時,是沒辦法直接接觸 layout 的 (除非你要在 readme 裡面叫使用者自己引用…),所以必須要有個方式把這些必要的外部資源塞進去使用者的 html 裡面。
關於這段「如何把外部資源塞到使用者的靜態檔中」,我參考了其他 plugin 的做法,發現大部份都是使用 hexo.extend.generator
來達成。不過我最後選擇其他做法來完成這件事。
Hexo 在編譯資源時,提供多種方式註冊自己的程式,來達到高度客製化。
其中 Generator
是用來產生檔案對應的路由,所以 Generator
都是回傳 { path: 'foo', data: 'foo' }
的格式,代表著 path 對應的 data 是什麼。
透過 Generator
可以做到 copy file 的功能,官方網站也有提供範例,再搭配註冊 tag ,就可以達成動態插入必要的外部資源。
1 | // generator that create a virtual path to external file |
如此一來,每當使用者插入 {% zoom %}
時,就會被展開成包含 include 外部資源的 html code,來達成目的。
然而剛剛的方式有些小缺點,比方說當使用者插入很多 {% zoom %}
的 tag 時,就會出現很多重複引用的程式碼,感覺也是怪怪的。
所以我最後利用另一種方式達到塞 code 的效果 - Filter
。
Hexo Filter 提供很多 hook 的註冊點,比方說在渲染 html 之前執行註冊的 function …等等。
我這邊用的是 after_generate
,就是在全部檔案產生完成之後執行,詳細 hook 名稱跟意義可參考文件
透過 after_generate filter
我可以在最後決定是否要插入外部資源 zoom.{js,css}
,實作如下:
1 | hexo.extend.filter.register('after_generate', () => { |
我去掃所有 html 檔案,並搜尋有沒有 div
class name 是 photozoom
的,如果有那就直接在 html body 插入所需的 javascript 跟 css 程式碼,非常暴力但還不錯~
且這作法同時兼顧如果有使用者想在非文章內容的地方使用 zoom.js
的效果,只需要在 <img>
中加上 photozoom
class name,在每次編譯時都會掃到並在需要的地方插入程式碼。
if (a && b)
這種判斷式,假設已經知道 A == false,那其實就可以不需要知道 b 的值,如此就可以直接忽略 b 而達到更快的知道這個判斷式是否為真[2]。底下是 C 的 strcmp
程式碼片段實作比較兩個字串是否一樣:
1 | int strcmp(const char *p1, const char *p2) |
從上面的邏輯可以看出來,如果第二個字元就不一樣的話,那我們馬上就可以結束整個邏輯然後返回兩個字串不一樣的結果,如此就能提升程式執行的速度。
而本文就是要來探討這種字串比對的方式所衍生的其他的安全性問題,也就是所謂的 Timing Attack
Timing Attack 其實就是所謂的時間差攻擊。
1 | do |
再來看一下剛剛字串比對的實作中的迴圈,由於這個迴圈實作的關係我們可以知道不同字串比對其實會花不一樣的時間,這很合理因為有時候比較字串到一半的時候我們就已經知道這兩個字串不一樣,所以提早返回結果。
那這樣到底有什麼安全性的問題呢?
試想,今天在輸入密碼的時候輸入錯了,結果電腦告訴你:「喔你第三個字元錯了。」
這樣其實蠻奇怪的吧?這表示假設駭客想要猜你的密碼,基本上他就可以先亂猜第一個字元,猜對之後再繼續猜一個字元…以此類推,那勢必可以破解密碼。
這個例子跟我們剛剛的字串比對其實基本上是同一件事情,因為如果你輸入密碼是錯的其實會比輸入正確密碼來的花更少的時間,因為錯誤密碼可能前幾個字元就錯所以提早返還結果。
雖然這時間上的差異幾乎微乎其微,但是只要多做幾次然後再平均一下,還是可以得出有意義的結果。
底下是一段程式碼來證明,只要多跑幾次就可以發現字串中不一樣的那個字元如果越後面字串比對的時間就會明顯有差異,利用這樣的資訊就可以慢慢推出答案的字串。
1 | std::vector<std::string> str{ |
這段程式會吐出以下的結果:
1 | x2345: 4.07176 s |
可以發現,比較的字串越後面才不一樣,花費越長的時間。這就是 Timing Attack 的主要概念。
那在實務上這個漏洞會出現在哪裡呢?
其實要先知道這個漏洞的意義:必須要是那個答案字串是敏感資料,像是密碼、或者某種 Token。 如果不是敏感資料那就算可以間接猜出來也是沒有什麼意義。
那就先來說說看最常見的密碼比對好了,現在隨處可見什麼帳號密碼登入,這種東西會不會踩到這個漏洞呢?就我的知識來講:基本上是不會。
因為假設是一個正常的後端工程師,他們不會去做所謂的密碼明文儲存。密碼這種東西即使在資料庫裡面也不會是明文儲存的,少說也是會經過一次雜湊而且還要加鹽。[3]
在資料庫存的會是
使用者輸入帳號密碼的時候,伺服器端會透過同樣的雜湊邏輯,就可以得出跟資料庫儲存的一樣的雜湊,這樣就完成一個正常的密碼驗證。
也由於 hashing 會讓輸入的字串跟得到的雜湊有很不一樣的結果,即使只改輸入的密碼一個字元,得到的 hash 也會完全不一樣。這樣的機制導致 Timing Attack 在這個例子上就完全沒有用了,因為攻擊者根本不能預期真正在做字串比對的那個雜湊是不是如攻擊者預期的一個字元一個字元改變,那如此即使有時間上的差異,也跟第幾個字元比對失敗沒有直接的關係。
那到底有沒有其他例子是真的會需要注意這個漏洞的呢?我能想到的大概就是像是某些加密貨幣交易所,他們的 API 幾乎都需要做所謂的簽章。概念如下:交易所需要透過 API key/secret 確保這個請求是來自合法的使用者,所以每個請求都必須附帶上簽章,公式大概是這樣:
這個的用意是使用者利用自己的私鑰去加密請求的參數,來證明自己是真的自己。
伺服器端則會用使用者本次請求的參數加上使用者的私鑰來去重組 Signature,假設 Signature 跟請求端附帶的一樣,那就是合法的請求。
在駭客的角度,由於沒有使用者的私鑰所以沒有辦法用正規途徑得到 Signature,但是利用 Timing Attack 這招就可以猜出本次請求所對應的 Signature 從而達到偽造身份的效果。但這有幾個不實際的地方:
講白了這個攻擊手段我個人覺得看起來很厲害但其實沒這麼可怕。除非是菜鳥工程師,不然實務上不太可能做出會被這個攻擊手段影響的系統…
對沒錯我就是奧客玩家!
本文提及的遊戲只代表曾經可以作弊,不代表現在或未來也可以。
說到這幾年陸續看到有隙可趁的遊戲,方法不外乎是兩個,一是藉由封包攔截查看內容再藉由修改封包資訊重送來攻擊,二是直接更改記憶體位置的值,這兩個方法都是非常直接暴力,案例很多,容我一一介紹。
其實這遊戲我已經寫過一篇文章了,但容我再次鞭屍 🤣
這遊戲是十分經典只要會察看網路封包,一定有辦法使用重送攻擊。到底什麼是查看封包再重送呢?來看看他的遊戲模式:基本上是關卡制塔防遊戲,勝利可以拿到獎勵。所以當我攔截到開始跟結束的封包訊息之後,我就可以直接自動刷關了。
1 | echo "Start event level=HELL......$i" |
可以看到所有的參數都是透過明文傳輸,唯一的保護是有使用 https,但這樣並無法防止封包被查看,因為只要建立 Proxy server 並自己簽署一個 SSL 憑證,還是可以偷看 https 的封包。
那如果我是開發者我會怎麼改善呢?比較基本的是加上 nonce
在 request 裡面並加密參數。nonce
本身是一個遞增數字,伺服器端須檢查 nonce
值要是遞增的不然就非合法。這種作法常見於加密貨幣交易所的 API 設計,這也是防止重送攻擊常見的手段。[1]
曾經風靡一陣子的遊戲王卡牌遊戲,同時也是伺服器設計有嚴重缺陷,嚴重到我覺得他們工程師可以東西收一收的程度。
這遊戲我開服三天就把全腳色練到滿等,同時集齊當時所有 UR SR 卡片,兩個禮拜後覺得太無聊直接棄坑,堪稱我玩過時間最短的遊戲。
其實他們犯了常見的錯誤,就是所謂 Atomic 操作原則。
有些事情是必須一起完成的,否則就應當全部都不算數。比如說:「我用十元買了一枝筆」,那我的錢包應該要減少十元,並且增加一枝筆。這其中每個動作缺一不可:如果錢包沒扣錢,則整段交易應該都要失敗並回復到原本的狀態。這很基本很合理。
此遊戲一樣是遊玩扣體力的機制,藉由有限體力來使一般人無法狂練等。但查看封包後發現,他開始關卡跟扣體力是兩個不同的 API,是透過遊戲先發送開始再發送扣體力的訊息,這樣一來我只要一直狂重送開始但不送扣體力,我就可以無限遊玩刷等。真的太扯了…..
另外在設計 server side service 時,有一個大原則就是不可以相信客戶端送來的資料,永遠必須做驗證。
而且客戶端不該發送結果,應該只發送欲執行的操作。用剛剛的例子來說:
「我用十元買了一枝筆,我的錢包減少十元,並且增加一枝筆。」→ 由客戶端計算結果是不可以的。
「我要用十元買一枝筆。」→ 只發送欲執行的操作,實際結果應由伺服器端執行,才能確保合法性。
唉,本遊戲真的是奇觀。
Messanger 兩年前推出可以在聊天室內玩籃球的彩蛋,浪費了我無數個小時在那邊投籃,其實也是有辦法作弊的。
前面提到我目前知道的方法不外乎是藉由封包攔截重送來攻擊或是直接更改記憶體位置的值,facebook 在中間人攻擊下了不少工夫,至少就我的觀察只要使用任何 Proxy server 似乎都會無法正常使用 Messanger,所以封包攔截重送這條路就很難走了。
剩下試試直接更改記憶體位置吧,基本上原理就是掃過全部此應用程式用到的記憶體位置並尋找指定的數值。比如說投籃好了,投進 1 分時先尋找記憶體中數值等於 1 的,再投進一次就進一步搜尋數值等於 2 的記憶體位置,以此類推直到精確地找到代表分數的記憶體位置,再修改其值。
很遺憾的是如果遊戲是單機遊戲,基本上是不可能防止記憶體竄改的[2],只能讓他變更難修改沒辦法完全防止。比較常見的做法是增加一個 dirty check 的檢查值,比如說分數的平方。在這樣的設計下,當你的分數被竄改成
不過其實這類的防禦性設計有時候根本不見得需要,以我上述提到的例子而言,除了遊戲王缺陷太嚴重以外,其實其他的都不見得需要做改善。像是 Metal Slug Attack 無法做到數據的篡改,能做到的只有自動花體力刷關;而 Messanger 的遊戲根本是類單機遊戲,就算拿高分也只是自爽。
所以到頭來,最有趣的還是尋找系統缺失的過程吧…😅
從以前當網管到現在工作一陣子之後,因為常常在家工作(加班?),也累積了不少存取內部資源的方式,本篇就是紀錄一下這些方式,以免我這個金魚腦以後又忘記….
使用 VPN 來存取 LAN 資源我想是最簡單直覺的了,前提是公司或實驗室有提供 VPN server 嘿。
阿假設你是系統管理者且你們想提供 VPN 服務,我會推薦使用 OpenVPN server,簡單易用。然後我之前有看到一個大神寫了個一鍵架設 VPN server 的 script
這個 script 從安裝,新增/刪除使用者, 應有盡有,堪稱無敵(?)
OpenVPN client 端設定很簡單,只要匯入預先產生的金鑰 (.ovpn
) 至 client 端應用程式即可。
OpenVPN client 端應用程式也是十分完備,無論 Windows/Mac/Android/iOS 全都有!真的可以做到隨時隨地,手機拿起來就可以工作…?
如果有時候你只是需要存取某台內部 server,只需要 terminal 環境,那其實直接使用 ssh 登入即可。如同引言所說,「通常一般的公司或者實驗室都會隔離內部資源,只留一個統一的對外出口」,那其實可以透過那台對望出口當作跳板,使用兩次 ssh 來做到登入你想要用的機器。
1 | ssh -i ssh_key_path -p port username@office.domain.com |
另外可以設定 ssh_config 來省去每次都要打一長串的指令,設定像這樣:
1 | Host office |
這樣就只需要輸入 ssh my_computer_in_lan
即可。
然後給個小建議,系統管理者在設定對外 ssh 服務時,盡量設定成只允許 ssh-key 登入,並且把 ssh port 改成別的 (不要用預設 22)。網路世界很可怕 der~~ 用預設設定就不要抱怨天天被掃 port 或被暴力破解密碼 (想當初實驗室某伺服器 root 帳號被暴力破解,最後只好重灌QQ)。
那假設你們的網管不願意提供 VPN 服務,你又想存取內部網頁之類的服務,匹如說內部自架的 GitLab,怎辦?
沒關係還是有招,這招叫做使用 ssh tunnel + browser proxy。聽起來很複雜?其實還好啦,這方法分為兩部分:
ssh 其實是一個很強大的工具,藉由他其實可以做到打開一個通道,從你的電腦連通道組織的單一的對外出口,變成說只要透過這個 tunnel ,就等同連結到公司的那台對外的電腦上。指令如下:
1 | ssh -i ssh_key_path \ # As usual, use ssh key to access is better |
這神奇指令幫你打通一個 socket5 的通道,然後再設定瀏覽器去使用這個 proxy,就可以達成跟 vpn 一樣的效果!
我這邊列出在 Mac 跟 Windows 上設定 proxy 的方式,但我想其他瀏覽器一定也有對應的方式設定。
open -a "Google Chrome" --args --proxy-server="socks5://localhost:12345"
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --proxy-server="socks5://localhost:12345"
有時候其實只是想用遠端電腦寫程式,用 ssh 當然也可,只是就是限制只能用 vim 之類的編輯器。
VSCode 最近推出了實用的 remote-ssh 功能,讓在家也可以使用遠端電腦開發。
可參考 ssh 那段來設定好 ssh_config,其他就如同平常使用 vscode 一樣,十分方便。
雜談:
從前從前,在我還小的時候(大概三四年前),我租了一台小的 Ubuntu cloud server 來跑一些小專案,從那時候開始我就一直想要一個可以監控程式或者伺服器的狀態的東西,那時候查到了 pm2 monitor 跟 netdata ,其中 netdata 那時候搞半天弄不好(現在有了 docker 大概一秒就搞定吧),再加上我比較在意我的 node.js 的程式狀態,最後選擇使用 pm2 monitor。
過一陣子後,正好有機會成為 CMLab 的網管,一下子要管的機器從原本自己的一台變成超過二十台,又重新讓我思考該怎麼才能好好監控這些機器的狀態。那時候前人(學長姊吧?)留下來的一個網頁,採用 snmp 協定將各台資料彙整至 web server,實作上使用 perl 彙整並產生 HTML 檔,呈現如下:
老實說雖然簡陋,但完全可以一目瞭然各台機器狀況,可以稱做一個不錯的監控系統了,那時候我也基於這個網頁再實作額外的自動警報系統,當記憶體或 CPU 用量過高時,發警訊到我的 Facebook Messanger 群組。順便呢也寫了一篇文章記錄: 用 Facebook 聊天機器人當通知系統
從那時候之後我就秉持著要啥自己幹的原則,又利用類似 Pull Based 的方式由一台機器整合的作法,實作出另一套專門監控多台 GPU 伺服器的 Multi-server GPU status monitor,實驗室大家看起來也是挺喜歡這個的 (還曾被教授關注表示讚 🥰),至今仍在運作也令我相當開心~
時間快轉到近期,又遇到一樣的狀況:同樣有一堆機器要監控。於是我就又再自幹了一套,只是這次實作方式並非是各自產生資料再由一台彙整的作法,而是一台機器主動去各台電腦撈取狀態。當然,這次重作絕對是做得比以前實驗室那用 perl 寫的來的好維護許多,外觀上也比較漂亮~
依舊是要啥自己做、高度客製化,整合所有我想知道的事情,稍微不一樣的是這次我將前後分離,資料搜集器負責蒐集我在意的資料 (metrics),存成 JSON file 直接由前端抓取,資料形式大致如下:
1 | [ |
這樣做的好處是這些 metrics 可以直接被其他人存取,像是我另外用 React Native 來做手機版的 Dashboard;以及一些 Alerter 就是直接讀取這個 JSON,有別於之前寫的 Facebook Messanger Alerter 去爬網頁才得到資料,少繞一圈。
然而就在最近,有同事就說:阿幹嘛不用 Prometheus ? 研究一下才發現,嗯…這東西真的很厲害 XD
有 Prometheus 當 metric collector,加上 Grafana 高度客製化的前端 Dashboard,要監控甚麼幾乎只剩要實作 exporter 而已(而且大部分狀況都有現成的)。
整個心路歷程走過來,從一開始用簡單的現成工具 => 自幹 => 自幹(前後端分離)=> 到最後又回到使用現成但更成熟的工具… 有種繞了一圈的感覺哈哈。
但我還是認真覺得我自己做的 Dashboard 比 Grafana 好看…
先來看一張 Slurm 架構的圖,基本上最重要的兩個東西就是 (1) Slurm Controller (slurmctld) 跟 (2) Slurm Compute Node (slurmd),Controller 是拿來分配任務用的,他管理所有 Compute Node,負責決定哪個任務該去哪個 Node 執行,而 Compute Node 就是真的會執行任務的機器。
所以要建置一個 Slurm Cluster,最少要弄一個 Controller 跟多個 Compute Nodes,至於其他像是 slurmdbd 等等,就並不是必需的東西。
在開始安裝 Controller 跟 Compute Node 之前,要先準備一些事情,
munge
,透過 apt-get install libmunge-dev libmunge2 munge
即可。slurm
帳號跟一個 munge
帳號,並且要在所有機器上都有這些帳號 (uid 也必須一致)。munge
是 slurm 拿來做 Authentication 的組件。
1 | # Create a slurm user, and change it to some id, the is must same across nodes. |
下載原始碼來編譯然後安裝
1 | cd /tmp; wget https://download.schedmd.com/slurm/slurm-18.08.2.tar.bz2; tar xvjf slurm-18.08.2.tar.bz2; cd slurm-18.08.2/ |
雖然 apt-get install slurm
有東西,但那個不是對的…
安裝好以後,可以透過一個網頁來設定基本的 config 檔,預設位置在 /usr/share/doc/slurmctld/slurm-wlm-configurator.html
,設定好以後存檔並放至 /etc/slurm-llnl/slurm.conf
。記得更改權限。然後就可以啟用。
1 | # set slurmctld & slurmdbd auto start via systemd (only for the controller) |
Slurm Compute Node 也可以透過 apt
安裝,但是由於我需要使用 slurm 的一些 api,所以這部分會使用從 source code 建置。
先安裝 munge
,改 user id 以及複製 munge key:
1 | # Copy munge key from slurm controller |
接下來先下載 slurm source code,並且 build
1 | # Install slurm |
到此基本完成 Slurm Cluster 的設定,可以透過一些指令來檢查 slurm 的狀態。
sinfo: 會顯示目前 cluster nodes 的狀態
1 | PARTITION AVAIL TIMELIMIT NODES STATE NODELIST |
squeue: 顯示目前正在執行/等待執行的任務
1 | JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON) |
scancel: 取消任務
還有很多其他指令,可以看這張 Cheat sheet。
使用的過程中總是會遇到一些奇奇怪怪的問題,這邊就列舉一些我常見的:
在使用 slurm 相關的指令時噴出的錯誤。
這個基本上是因為 Authentication 出錯,把所有 nodes 的 munge 重啟就會解決。
有時候會發現有一些 Job 就是一直卡在 Completing (CG state),這時候把那個 node 設為 down 再設為 resume 就會消失了。
1 | $ scontrol update nodename=research04 state=down reason=job_stuck; |
這表示有些 node 沒有 slurm, munge user,或者他們的 uid 不一致,解決方法就是把他們設為一致。
後來稍微多重開機試幾次發現這種狀況有時候會消失,查一下網路發現大概有幾種可能,一是主機板壞了,二是鍵盤跟滑鼠的連接到主機板的排線壞了。但是這兩種壞法修的價格可差距不少,所以我就堅信只是排線壞了(X
既然都免不了要維修一趟了,又由於官方的螢幕脫膜更換計畫規定是購買日四年內可免費更換,我這台剛好接近過期,就想說順便將我螢幕也換掉。所以我打算維修的問題就有兩件:
但由於我螢幕脫膜的情況非常輕微,大概只有一兩根細線般的脫落,所以我先跑去學校內的授權店詢問這種狀況是不是可以更換,結果馬上被打槍說這麼輕微不可能可以換,不信自己去直營店問。
既然都被這麼打槍了,我就只好轉戰 101 直營店拉…
久聞直營店非常難排到維修,我每天開 Genius Bar 預約維修,然後無論平日假日時段永遠是滿的,非常驚人…後來發現據說 iPhone 有 App 可以比網頁版多提前預約一天,根本歧視非 iPhone 用戶…難怪網頁版永遠預約不到。嘗試預約兩三天後我就放棄了,打算直接耗一整天去現場排候補維修名額。就這樣,我大約中午時刻到達 101 直營店,進門右手邊就是 Genius Bar 排隊的地方。排到以後會寄一封簡訊來,並在快輪到時會再寄一封來提醒,我自己大概是等了五個小時才輪到我…
輪到我之後就要我描述狀況,可以的話儘量直接重現問題給他們看,就會減少很多檢測,剛好我那天鍵盤滑鼠依舊是不能使用的狀況,所以他們只跑簡單的檢測就結束了。(為了跑檢測他們還跑去拿外接鍵盤跟滑鼠 XD)
另外關於螢幕脫膜的部分二話不說直接跟我說可以換~
後來在報價時跟我說可能要換主機板,我就說我只想換排線跟螢幕,如果要修主機板我就不修了。店員也就如實把我的需求寫進備註。
送修之後雖然說 3-5 天會打給我告知維修情況,但根本沒打來…
過了大概 20 天之後才寄信來說可以取貨,也附上維修明細。
總結來說 Apple 直營店的服務還算不錯,更換鍍膜完全不會阻撓,檢測維修時也十分迅速。缺點就是預約維修太難,現場排也要超久真的很花時間…
所以若需要去直營店維修的話有幾個建議:
雜談
用 Python 可以簡單幾行就寫出來:
1 | def fizz_buzz(num): |
不過有狂人就把這當作分類問題,用 tensorflow 來解這個問題,原文在此,是篇很有趣的文章 XD
由於原文是用 tensorflow 實作,我想我就來寫個 PyTorch 版練習一下吧!
基本上就是把 FizzBuzz 當作分類問題 (Classification) 來訓練,要做的事大概有這些:
那就來一步一步看看
雖然 FizzBuzz 輸入是一個整數,但是把他轉成二進位會比較好訓練,所以先來寫個轉二進位的函式:
1 | def encode(num): |
因為我不想 import numpy
,所以這邊轉二進位的方式是用 Python 的 format 來做。
另外還要把 FizzBuzz 改寫成回傳分類號碼:
1 | def fizz_buzz(num): |
接下來要來產生資料拉
1 | def make_data(num_of_data, batch_size): |
由於 training 的時候通常會是一批一批 (batch) 下去訓練的,所以在準備資料時就先一批一批放在一起會比較方便。
所以改一下,
1 | def make_data(num_of_data, batch_size): |
前置步驟都弄好之後,終於可以來產生訓練跟測試資料拉,Batch size 就訂個 32 好了:
1 | training_data = make_data(1000, 32) |
其實我對於如何設計 model 還是沒有很了解,不過這問題應該是挺簡單的,弄個幾層 fully-connected layer 應該就夠了吧?
1 | class FizzBuzz(nn.Module): |
我用了一層隱藏層,1024 個神經元,activation function 則都是最基本的 ReLU 。in_channel
, out_channel
分別是輸入數字是長度多少的二進位 (10),以及輸出幾種分類 (4)。
PyTorch 的 model 是繼承 torch.nn.Module
來寫個 class,通常只要定義 __init()__
跟 forward()
就好,如果要自己做特殊的 backward 的話,也可以實作 backward()
。
整個訓練的過程基本就是按照一般的分類問題流程做,把資料丟進 model 的到預測,把預測跟正確答案做 cross entropy 當作 loss ,然後去最小化這個 loss
用 PyTorch 寫大概是這樣:
1 | def training(model, optimizer, training_data): |
由於 要是 Variable
才能自動算 back propagation ,所以 data 跟 label 都要變成 Variable
。
這邊我用的 optimize 方法是 Stochastic Gradient Descent (SGD),記得每次都要先清空 gradient 再做 backward。
萬事皆備,可以開始來看看結果如何了,來 train 300 個 Epoch 好了,
1 | ==== Start Training ==== |
哇!才 98% 準確率呢… 拿去 online judge 解題大概不會過呢 XD
如果想要玩玩看我的 code,這邊可以看:
https://github.com/SSARCandy/pytorch_fizzbuzz
JavaScript,一個為了網頁互動而誕生的腳本語言,最早是因為 Netscape 開發了一個比較成熟的瀏覽器 Navigator,但由於沒有可以讓網頁與使用者互動的方式,所以他們就開發了 JavaScript 來當作網頁的腳本語言,其中主要開發者是 Brendan Eich。
由於當時物件導向正夯,Brendan Eich 也決定讓 JavaScript 所有東西都是 Object。
如此用途明確的語言,似乎不太需要非常完整的底層架構吧?用不著像 C++, Java 這種泛用式程式語言一樣完整,所以 Brendan Eich 並不打算引入 Class 的概念。但又由於JavaScript 所有東西都是 Object,勢必要有種方法做到類似繼承這件事。
所以原型鍊就出現了!
JavaScript 要建構一個 instance 會用 new
關鍵字,但實際上這 new
跟 C++, Java 的不一樣。
JavaScript 的 new 其實後面接的是一個 function,類似於 C++ constroctor
1 | function Person(name) { |
上面這段例子是 JavaScript 創造實例的方式。
可以看到 Person
中有兩個東西,一個是 name, 另一個是 Person 的 method sayHi
,雖然這樣很好了,但是這樣 man1
, man2
中其實包含了一樣的 sayHi
function,浪費記憶體空間。
所以如果要做一個類別共用的方法可以這樣:
1 | function Person(name) { |
把類別共用的 function 寫在 prototype
中就可以達成共用的效果,而其實這個 prototype
就是原型鍊。
我們用瀏覽器偵錯模式印變數的時候,相信經常看到 __proto__
藏在變數裡,那個東東就是原型鍊。
JavaScript 中有幾個預設的類別,像是 Object, Array 等等,我們在宣告變數的時候其實裡面都會藉由原型鍊 串 到預設的類別。
所以其實在創造實例時,JavaScript 會把 __proto__
指向他的原型,以空物件 {}
而言,就是預設類別 Object
。
回到上面 Person 的例子,他的原型鍊就會是長這樣:
可以發現寫在 Person.prototype
的 sayHi
,實際上是定義在 man.__proto__.sayHi
,也就是 Person 的原型,而在呼叫 man.sayHi()
時,由於找不到,所以 JavaScript 會藉由__proto__
嘗試往上找,就會在 man.__proto__
中找到。
而這個一直往上一層原型找的過程,其實就模擬了繼承的效果。
雖說 JavaScript 當初沒有 Class 的概念,但在 ES6 中其實出現 class 關鍵字了,但其實這只是一個語法糖而已,可以藉由幾個例子發現 ES6 背後還是透過原型鍊來運作。
1 | class level1 { |
上面這個例子是用 ES6 寫的繼承小程式, level2 繼承 level1 。直接來看看創造出的實例 l2
裡面是什麼:
又看到原型鍊了!l2
的原型鍊串成這樣: l2
→ level1
→ Object
。
看看 getX
, getY
就會發現他們定義在不同層級,因為 getX
是父類別的方法,所以在原型鍊中的更上一層。
由此就可以看出 ES6 雖然有 class 關鍵字,但其實原理還是原型鍊。
雜談
除了學校教的 C/C++ 以外,我似乎沒去搞懂過其他語言背後的邏輯,秉持者會用就好的心態活到現在(X
這次稍微理解原型鍊以後,好像又更了解一點 JavaScript 了呢~!
這篇論文[1] (Deep CORAL: Correlation Alignment for Deep Domain Adaptation, B Sun, K Saenko, ECCV 2016) 提出一個 CORAL loss,通過對 source domain 和 target domain 進行線性變換來將他們各自的的二階統計量對齊 (minimizing the difference between source/target correlations).
作者將 Deep CORAL 應用在一般的分類問題上,整個神經網路架構如圖。從中間 cov1
~ fc8
其實就是一般的 AlexNet,只是稍作修改改成有兩個 input (source data & target data) 以及兩個 output。
在訓練的過程中,每個 batch 都包含了 source data & target data,其中 source data 是包含 label 資料的;而 target data 則完全沒有 label 資料。
source data & target data 各自經過一 shared weight 的 networks 之後會有兩個 output,其中:
fc8
及 target 的 fc8
會再拿來算 CORAL loss而總和 loss 為兩者相加:
作者提出的 CORAL loss 是在計算 source & target covariance matrix 之間的 distance。
We define the CORAL loss as the distance between the second-order statistics
(covariances) of the source and target features.
而這個 loss function 定義如下:
其中,
詳細符號定義可以參考 paper[1] section 3.1
至於 gradient 可以由 chain rule 算出來,如下:
注意 target 那邊是有個負號的,當初在實作時忘記這個負號而搞半天…
作者做的實驗也是在分類問題上,架構如同上面提及的神經網路架構圖。
實驗採用 Office31 dataset[3],這是一個專門拿來做 domain adaption 的資料集,裡面有三種不同 domain 的影像: Amazon, DSLR, and Webcam
裡面都有相同的 31 種類別,也就是說這三大類唯一不同的點就是圖片的樣貌:
在實驗進行過程中,source data 會有 label;而 target data 則沒有。
且在開始之前會先預載 ImageNet pre-trained model。
由於 Office31 有三種 domain data,所以作者就做了所有 domain adaption 的組合,以下是結果圖:
可以看到 D-CORAL 在大部分的 domain adaption tasks 中都取得了最好的成績。
再來看看其中一個實驗 Amazon → Webcam 的詳細結果:
圖 (a) 比較了有 CORAL loss 與沒有 CORAL loss 的差別,可以看到當加入CORAL loss 之後,target (test) task 有顯著的提升,而且並未使得 source (training) task 的準確率下降太多。
圖 (b) 則可以看出,classification loss 跟 CORAL loss 其實是扮演互相抗衡的腳色,隨著訓練的進行會讓兩者到達一穩定的狀態。
我也試著用 PyTorch 實做了此篇論文的方法,最重要的其實就是新增一 loss function 到整個網路架構中,其中 forward and backward 的算法剛好也有詳細說明。
Forward 的部分大概如下:
1 | def forward(self, source, target): |
Backward 則如下:
1 | def backward(self, grad_output): |
寫起來公式的部分又臭又長 XD
我也實際跑了 Amazon → Webcam 的例子,做了個圖:
可以看出有 CORAL loss 的確使得 target task 的準確率提升一些。
不過我做出來的整體準確率並沒有與論文上的一樣有 60% 左右,而是大概在 50% 左右,不知道為甚麼… QQ
經過 redhat12345 的建議後,修正了一下 CORAL Loss 的算法,終於使 Target accuracy 提升到原論文的程度。
1 | def CORAL(source, target): |
但是我的桌機都是 Windows,在可以用桌機的環境下卻必須使用小小的 Mac 打字真的不是很高興…
正好最近從學長那邊得知有個方法可以讓 Windows 使用 PyTorch ,就趕緊來試試!
Window 10 現在有個東西叫 Windows Subsystem for Linux (WSL) ,是一個在 Windows 下的 Ubuntu 子系統,這個子系統可以做到任何正常 Ubuntu 做得到的事。
那我就可以在 WSL 中按照 Linux 的流程設定好 PyTorch 的相關環境,然後在 Windows 中使用 WSL 的 Python 環境,就可以達到目的 (讓 Windows 使用 PyTorch)。
所以基本上環境設置步驟:
OptionalFeatures
指令,會跳出一個視窗完成以後可能需要重開機。
接下來是要在 WSL 中設置 Python 以及 PyTorch 的相關環境。
如果沒有 Python 記得先安裝。
然後安裝 PyTorch,基本上按照 PyTorch 官方網站 教學操作:
1 | $ pip install http://download.pytorch.org/whl/cu75/torch-0.2.0.post3-cp27-cp27mu-manylinux1_x86_64.whl |
PyCharm 是一個可以寫 Python 的 IDE,雖然專業版要錢,不過學生免費~YA!
安裝就不贅述了,反正就是一直下一步…
由於要用 WSL 裡面的 python,所以必須設定 Remote Python Interpreter
由於是要透過 ssh 去存取 WSL 中的 Python ,所以 WSL 那邊要開啟 ssh service 好讓 PyCharm 連線。
在 WSL 中:
1 | $ sudo service ssh start |
WSL 其實是可以存取本機 (Windows) 的資料的,預設 C 槽是掛載在 /mnt/c
這也要設定一下才能讓 PyCharm 運作正常:
C: → /mnt/c
1 | $ sudo service ssh start |
去更改 /etc/ssh/sshd_config
:
1 | PasswordAuthentication yes |
基本上最重要的就是換個 Port 了,會沒辦法啟動大概是本機 (Windows) 有程式已經占用 Port 22 了。
1 | $ pip install torch-xxx.whl |
請檢察 pip -V
版本,起碼要是 9.0 以上,可以用以下方法更新 pip
:
1 | $ pip install --upgrade pip |
對,就像是 cml-status 一樣,
假設有個 GPU 版的 cml-status,應該就可以讓大家更輕易地找到閒置的 GPU,如果有人佔用過多運算資源也容易發現。
於是 CMLab GPU Status 就誕生拉~
由於我們有數台伺服器是有 GPU 資源的,所以要做出一個網頁版的監控系統大概要有兩個步驟:
對於第一點大概有兩種做法,一是主動去取得資訊,也就是透過 ssh 登入到各個有 GPU 的伺服器詢問資訊;二是各個有 GPU 的伺服器各自回報資訊給某一台來彙整。
而主動去取得資訊的方法有幾個缺點,
另一個方法則是「各自回報,統一呈現」,
就是大家各自回報 GPU 狀況,
然後由 web server 統一彙整資訊,
這種感覺就比較人道一點,大家一起分擔工作~
決定了大方向的做法以後,可以繼續切分整件事情的流程:
幸好我們的 server 都有用 NFS ,所以各自回報到 NFS 上就可以讓其他台存取到資訊了。
那獲得 GPU 資訊的方法不外乎就是下 nvidia-smi
來取得囉,但說真的這指令太豐富了,所以我改用別的神人做的指令 gpustat,輸出就乾淨多了~
所以每一台 GPU server 要做的是「每分鐘回報一次 GPU status 並存至 NFS」,可以透過 crontab
註冊:
1 | # crontab on each GPU server |
剛好我們 server 名子都是很沒創意的 cml*,所以彙整相當簡單。
由於各自回報的關係,在 NFS 上會有如下的檔案:
1 | -rw-r--r-- 1 root root 875 Aug 25 23:04 cml10 |
那要彙整就下個 cat cml*
就解決了。
最後有了彙整後的資訊後該如何呈現置網頁上呢?
由於我們的 web server 有 apache,所以基本上只要多搞個資料夾底下有 index.html
就可以了。
所以只要想辦法將彙整的資訊轉成 html 即可。
網路上大神很多,我又發現了 ansi2html.sh ,這工具可以把 terminal output 轉成 html ,並且連顏色都幫你轉成 css ,太神拉~
所以要變成網頁呈現就可以註冊個 crontab
:
1 | # crontab on web server |
每分鐘重新刷新 index.html
做好以後還是逃不掉 BUG 的摧殘QQ
有時候會發現彙整的資訊會缺少某幾台 GPU server 的資訊
查來查去發現原來是因為 crontab
註冊的時間一樣(都是每分鐘),再加上 NFS 是透過網路傳輸所以會比較慢,導致各自機器每分鐘回報狀況時檔案還沒寫入,web server 就執行彙整動作,就會出現缺檔的情形。
解決方式很簡單,就是彙整時間稍微延遲一點,讓各自回報有時間完成。
1 | # crontab on web server |
用 sleep 即可延遲指令。
雜談
這看起來是基本中的基本,但我當初就是沒做(掩面
當初只有口頭說好而已。
這件事情之所以重要,是因為這是個最基本的保障,不論是對我們或是對客戶方,
和約可以說清楚講明白我們到底要做什麼?哪些是我們的負責範圍、哪些不是?
然後當然還有其他的細節,包含價格,時程等等都應該要寫在合約。
在這次的案子中我遇到最大的問題就是:我們開發好了 ,但是客戶卻一直沒時間驗收,導致每次來回 feedback 就可能拖了好幾個月。
這樣造成很多問題一個是實在是拖太久了,隔好幾個月才又重起專案做起來真的很煩;另一方面客戶方也會覺得我們做得很慢。因為我們做出來的東西不一定是客戶想要的,一來一回修正就花掉太多時間,整體而言就會有我們也開發得很慢的感覺。
如果當初可以在合約中訂好幾次 check point 的時間點,至少就可以確保不會無限期拖延下去。
我這邊指得對口是指:「 可以快速獲得 feedback 的人」。
因為我們這一次的開發包含了所有的設計,都是我們負責。但是呢,扯到設計這種比較主觀的東西,我們做出來的並不一定客戶接受。所以與其等到 check point 再來被客戶打槍要求重做,不如當初就先確保一個客戶方的對口,才可以在我們做 prototype 時就迅速的確認到底是不是符合他們想要的。
簡單來說,確認對口就是可以使我們少做很多白工。
這個點其實跟上一點有一點關係。案子開發到後期勢必是有一些 BUG 或其他要調整的部分。所以也是必須要追蹤這些 issue。
那當然大家都認為 github issue tracker 很好用很棒,但事實上客戶這邊根本不會使用這種工具,最後就會變成他們根本沒在看。
所以與其用我們工程師覺得好用的工具,不如使用大家都會用的工具。像是在這一次的案子裡我的 issue tracker 是簡單的用 Google sheet 拉一拉表格,這樣反而更能讓客戶清楚知道我們的進度。
即時通訊也是,工程師可能覺得 Slack 很棒很適合案子的溝通,但事實上就是客戶那邊永遠都不會上線 XD
所以反而使用私人的 Facebook Messanger 還比較有效率。
有一件事情我體會深刻,就是「錢的事情一定要講清楚」。
也不要覺得自己的開價會不會太貴,因為會不會太貴是客戶那邊該煩惱的 XD
雖然每一個案子可能順利的程度不同,但是我個人認為接案一定會比想像中的麻煩。
所以在錢的方面一定要開一個確保自己不會做到覺得「很不划算」。
具體而言大概就是你覺得可以的價格再乘以 1.5 倍,到時候你會覺得這才只是「剛剛好的價格」。
另外,錢也是應該要有先付、後付的部分,才比較有點保障,至少不會一毛錢都拿不到的風險。
像我這一次的案子就因為也沒有寫合約所以最後變成是結案了才要付錢,那這樣子其實對於接案方非常虧,因為他們隨時可以突然說案子不要做了,然後我就會變成做一堆但啥都拿不到。
總而言之,錢的事情就幾個原則:
雜談