Category: 筆記

  • 在windows console app模擬sigterm

    posix相容的系統上,想要把一個程式用正常的流程關掉可以透過 `kill -SIGTERM $pid` 來傳送signal,然後在目標程式上trap signal來進行收尾 但在windows上不管是用 `taskkill /F` 還是 `wmic call terminate` 或 `wmic delete`,程式都會被強制關閉(相當於sigkill),沒辦法執行收尾的流程 `taskkill` 不加F的話是正常關閉,但如果對象是console app時,他會說這個程式只能使用F的方式關閉 查了半天,最接近sigterm的操作是interrupt(ctrl+c)或是break(ctrl+break) 但這個event必須透過win32 api來傳送,因此只能用powershell或是做exe來達成 最後我選了用c的方式實作 原本想要直接傳送event給指定process,但GenerateConsoleCtrlEvent只能送給process group id,而process group id無從查起,直接用pid也沒辦法,只好設定成0讓他傳給當下process group中所有程式 但這樣想要關閉的程式還是會收不到,所以還需要FreeConsole和AttachConsole讓執行當下可以跟目標process掛在同一個process group底下,這樣event就可以收到了 這時會發現送event的程式執行到一半反而因為自己收到ctrl+c而中斷,為了讓他能正常結束要先對ctrl+c免疫,因此要加上一個假的event handler 到這邊這隻程式就能正確發送ctrl+c給指定的pid,唯一需要注意的是整個process group都會收到,地圖砲範圍太大,發動之前要先提醒友軍(不想被關掉的程式)迴避,不然大家都一起被關掉了

  • ingress nginx zero downtime deployment

    參考來源:https://github.com/kubernetes/kubernetes/issues/85643 Load Balancing運作 pod在沒有VPC native IP的情況下,必須透過NodePort將pod從node的port上expose出來給前面的cloud load balancer使用 雖然AWS的network load balancer,可以保留原始的client IP直接傳到node上,但LB上註冊的InstanceTarget其實是所有node externalTrafficPolicy為Cluster時,LB上的node health check會故意全部失敗,讓LB把流量發給所有的node,node再利用kube-proxy轉發給實際有目標pod的node 要是LB使用到的node並沒有目標pod,kube-proxy進一步轉發流量會造成TCP source IP被洗掉,就拿不到client IP externalTrafficPolicy為Local時,LB上的node health check會顯示實際有pod的node,因此LB可以正確的將第一手source IP送到pod手上 當pod需要shutdown時,k8s會將NodePort關掉,並讓pod進入terminating狀態 問題 使用ingress nginx時,如果希望$remote_addr能直接抓到真的client IP,一般會考慮將externalTrafficPolicy調成Local 但在pod關掉時,LB那邊沒辦法及時知道NodePort已經被k8s關掉,會繼續轉發流量到該node上 一直要到LB自己的health check失敗才會停,這過程可能有20~30秒的時間會導致部分流量無法被正確處理 externalTrafficPolicy改成Cluster雖然可以解決流量遺失的問題,但不一定能拿到原始的client IP 解法 幸好ingress nginx有real ip module可以辨識proxy protocol,而AWS的TCP LB也支援向後送出proxy protocol 因此只要用externalTrafficPolicy=Cluster配上proxy protocol,就能保留client IP又同時支援node之間互傳,達到zero downtime deployment 其他情境 本來有想過改用native IP或許可以解決這個問題 實際測試後發現,AWS的LB之所以可以保留client IP是因為在VPC內連線到instance不是用instance IP,而是透過instance ID 但pod不屬於instance,沒有instance ID,LB必須以正規的TCP連線傳到pod的native…

  • Dual stack OpenVPN 預設閘道切換不完全

    以往純IPv4的環境下,要讓client把所有的流量轉送到VPN的話,會在server上用下面這個語法,告訴client要把預設閘道轉到VPN上 如果server同時有IPv4和IPv6,就會改用這個語法,同時相容兩種client 最近觀察到手機在連線的時候會出現這樣的訊息 NOTE: unable to redirect IPv4 default gateway – Cannot obtain current remote host address 連上之後,會發現只有IPv6走VPN,IPv4一樣是原本的行動網路 研究之後判斷是client端使用dns name連線到server時,會優先解析出IPv6,然後使用IPv6連線 因此client端知道server的IPv6地址,但不知道IPv4地址 收到server說要切換閘道的時候,需要先建立一個static route 而client因為不知道IPv4地址,所以只會在IPv6建route,並把default gateway轉過去 這部分的解法我是直接從server多推幾組default route 因為IPv6已經有打通的VPN連線,直接叫client把資料丟到tun device走VPN過去 這樣就能讓流量都經過VPN了

  • 虛擬主機設計與規劃

    最近替主機換新的硬體,順便重新檢視虛擬主機的設計,做些改良後決定寫下來 使用者帳號與網頁檔案 因為規模小,使用者採用unix系統帳戶來管理,網站使用以下的格式放置在每個人的家目錄裡面 /home/{user_name}/sites/{domain_name}/ 每個網站的目錄底下各會有「backup」「log」「temp」「webroot」四個資料夾 「backup」是放定期打包好的網站目錄,畢竟直接用FTP拉一堆資料夾效率很差 「log」有apache_access、apache_error、php_error的檔案,並會定期做logrotate 「temp」是開給php放暫存的檔案,不論是上傳檔案、session、opcache通通在裡面 「webroot」顧名思義就是放網站的地方 整體而言會需要存取家目錄的程式有「httpd」「php-fpm」「vsftpd」「logrotate」這四個 這邊採取的安全措施有三種,略分為「file permission」「software access control」「SELinux context」 「file permission」就是磁碟的權限,家目錄下一律是使用者擁有自己的檔案 「httpd」主動加入使用者的群組,並將家目錄設為群組可讀 「php-fpm」直接以使用者的身分啟動 process 「vsftpd」使用 chroot_local_user 切換成使用者的身分 「logrotate」是 root 身分不受權限影響 「software access control」是軟體本身定義存取的範圍 「httpd」設定 DocumentRoot 在 webroot 「php-fpm」利用 open_basedir 把存取範圍限制在 temp 和 webroot 「vsftpd」使用 chroot 限制在家目錄裡面 「logrotate」從 config 定義 log 的位置 「SELinux context」是系統對軟體的存取控制,可輔助磁碟權限的不足,我對家目錄用了以下規則 semanage fcontext -a -t httpd_log_t ‘/home/[^/]+/sites/[^/]+/log(/.*)?’…

  • Rails container 在 Google Kubernetes Engine 上不正常關閉

    最近在部署rails專案到GKE上,發現rolling update在關掉container時會跳出「Pod errors: Error with exit code 1」,然後狀態變成紅色的 雖然只是要被關閉的pod壞掉不影響更新流程,但偶爾會造成服務中斷,看起來不是件好事 最初推測是puma在關閉的時候有什麼問題,找到一個issue說bundler在1.16版以前跑「bundle exec」會造成puma收到SIGTERM後回傳exit code 1 這現象剛好很符合遇到的情境,但我用的是1.16版以後的bundler,而且測試發現,bash上跑的puma收到SIGTERM會正確的回傳code 143,唯有放在container裡用docker開起來,傳送SIGTERM才會遇到code 1的結果 當時完全沒有頭緒,過了幾天同事剛好提到說他以前有看過一個叫做dumb-init的東西 dumb-init是設計給container用的init process,有回收process和轉送signal的功能,至於為什麼container還需要init process的原因可以看這一篇文章的說明 查了之後覺得可能很有幫助,畢竟bundler跟puma都曾有過signal處理不好的問題,而且在bash上可以正確關閉,代表直接把puma放在PID 1會缺某種東西 一試用下去,puma還真的能正確回傳code 143了,正想說可以放上GKE的時候,GKE又亮紅燈了…. 他對143這個回答也不高興Orz code 143是由128 + 15組成,signal 15也就是SIGTERM,不一定是異常的狀態 為了要讓GKE滿意,只好想怎麼把code變成0 翻了翻手冊並測試確定puma在收到SIGINT時會回傳code 0,剛好dumb-init有支援signal rewrite,可以直接把SIGTERM改成SIGINT傳給puma 於是最終透過dumb-init成功解決了exit code造成GKE不爽的問題 另外有個值得注意的地方是docker的entrypoint跟cmd分別等於kubernetes的command跟args,如果設錯會產生不同的效果,細節可以看手冊的Notes

  • Database Query 最佳化實記

    兩三個月前遇到一個嚴重拖垮公司資料庫的query,好死不死那個API又是流量主要會出現的點,身為社畜只好想辦法把它解決掉,就這樣我看了一整周的db console和mysql手冊,最後想出一個只需最小修改就能達成的最佳化方案 先說明一下問題點的架構,這邊的設計以正規化來說我是覺得沒問題的 食譜和分類是傳統的多對多關係,因此會有一張中間表;分類本身則是有自我關聯的上下層關係 應用上的需求是這樣的:給定某個分類,找出該分類(置頂與不置頂)及其子分類(不置頂)下的所有食譜(不能重複),並以「是否置頂」和「發表時間」進行排序 系統原設計是先抓出所有分類ID(某分類及其子分類)後,才進行傳統的join去從分類ID找食譜,這部分因為分類數量不會很大,所以我認為還堪用就不進行修改 真正的瓶頸在後面,當時query長這個樣子 一開始的想法是order由兩張表的欄位組成,照手冊的說法不會利用到index,所以試著在category_recipes上面做非正規化,加了一個published_at,值則是利用model callback從recipe同步過來 可惜這樣跑起來還是不快 後來想到distinct比較的是所有欄位,但index裡面不可能有所有欄位,就往group的方向去思考 反正只要id是唯一的,得到的東西就不會重複 於是query變成這樣 雖然感覺方向對了,但效能還是沒到理想的狀態 手冊又翻了半天才理解出: – index 的利用是從左到右 – GROUP BY 和 ORDER BY 共用同一個 index – GROUP BY 和 ORDER BY 如果有任何一個方向不同就不能用 index 如果能在recipe_id之前用is_sticky和published_at做group的話不是就能用到index了? 很幸運的實驗有成功,資料結果和排序也正確 query最終變成了這樣 index的部分我試出這幾種,第一種是效率最高的,雖然只能用到OFFSET 330左右但很夠了 OFFSET如果更大的話,資料庫會選擇其他兩種並建立暫存表和使用filesort 到這邊總算解決query時的效能問題…以為就這樣結束了嗎? 改完後發現分頁套件(kaminari)在計算總數的時候會觸發ActiveRecord組錯SQL的現象,一查才發現是它不支援descending count,原本還想去修,但我才發現group根本不能拿來count我要的東西 為什麼?因為我想count的是總數,並不是group完分別的數字,因此計算總數的時候還是要回去用distinct 這邊有個比較簡單的想法,因為ActiveRecord的count屬於terminal method(終點站),所以可以直接在上面做切換機制 但問題又來了,ActiveRecord的語法是用串的,我要怎麼知道先前有串過group?而且不是所有group都需要換回去distinct,該怎麼分辨? 後來想到的方法是透過SQL comment傳遞訊息,直接在group裡面給字串的時候插一個特定的comment進去,再由count那邊去判斷就好了 到這邊才成功解決這個query的效能問題,資料庫CPU使用率和反應速度都有大幅改進,使用者終於不用等到斷線了QQ 2019/01/10 更新 後來遇到特定分類在抓資料的時候無法使用效率最好的index,會產生不必要的系統負擔 解法是透過FORCE INDEX指定query能用的index,避免optimizer選了錯誤的選項…

  • Ruby on Rails after_create_commit 重複執行

    在處理after_create_commit這個callback時,發現會有重複執行的問題,最初觀察到的現象是會執行兩次 經過多次修改嘗試後,確定問題是出在callback內更新,單純在裡面放logger丟訊息並不會重複 google後才發現這問題從Rails 4.0就有了(汗 後續也不斷有issue被提出來,不過都沒有去解決 要重現這個問題很簡單,只要弄一個像這樣的Model,然後對它create就能看到無限迴圈 目前我的解法是透過after_commit_everywhere這個套件,將程式碼直接掛在當下執行時的transaction上,由於不牽涉到rails本身的transaction運作,所以不會遇到重複的雷 還好最近終於有人去修這個問題了(ae028984),不過因為是剛弄上去的,要包進release可能還要些時間

  • Mail server on Fedora 26

    備註:這篇是修改自三年前的「Mail server on CentOS 6」,加上些新的內容和補充 本文開始~~ 一般來說email是由三個部分組成,Mail (Transfer/Delivery/User) Agent,為什麼wiki上說有五種呢?因為MSA和MRA通常是包含在前面三種裡 在伺服端的重點是MTA跟MDA,負責收發跟儲存信的兩個部分,這邊我們用postfix和dovecot來實作 套件 在開始之前,先安裝必須的軟體 SSL憑證 由於這個教學會設定成提供加密連線,憑證是必需品 dovecot在安裝後會自動產生一組self-signed cert,放在/etc/pki/dovecot裡面,不過還是建議向CA申請,避免使用者在連線時會出現錯誤 向CA拿到憑證之後,就直接放在/etc/pki/dovecot/certs/裡面,私鑰則是/etc/pki/dovecot/private/ 如果CA有中繼憑證(intermediate certificate)的話,只需把中繼憑證以倒敘的方式append在自己的憑證後面即可 最後,檢查憑證跟私鑰的selinux context和存取權限,權限建議是root:root 600 selinux在enforcing的情況下,context如果不是「unconfined_u:object_r:dovecot_cert_t:s0」,會造成dovecot無法啟動的狀況(permission denied) 如果使用Let’s encrypt作為CA的話,可以不用搬到/etc/pki/dovecot/裡面,直接指向/etc/letsencrypt底下的憑證即可 MySQL 首先是產生所需的db和table,使用mysql root來執行以下語句 到這邊就新增完成了,記得先用select看一下DB的內容是否正確 Postfix 首先到/etc/postfix/main.cf去看看 基本設定主要是和本機(localhost)有關,我們的信箱會使用虛擬網域來收發信 在這之前,先把驗證和加密的功能設定好 再來是設定虛擬信箱 馬上就來新增剛剛寫的mysql檔案,總共有三個 存好之後使用chmod修改存取權限為600以免DB密碼被看到 然後使用以下指令來驗證上述三個檔案是不是正確設定 如果設定正確,應該會看到網域或是信箱,什麼都沒有代表失敗,就要檢查設定或是資料庫是否有問題 最後修改/etc/postfix/master.cf,開放465和587的port Postfix到這邊就完成了,記得要替防火牆開port 25, 465, 587,然後重開postfix服務 Dovecot dovecot的主要設定都在conf.d裡,我們逐步來修改 來弄資料夾的部分 過來是驗證的部分 來到剛剛引入的auth-sql.conf.ext 沿著剛剛的args繼續往下做回到上一層新增dovecot-sql.conf.ext pass_scheme選SHA512-CRYPT的原因是插入使用者資料時使用$6$作為參數呼叫ENCRYPT(),libc的crypt()在hash時會知道我們要使用SHA512 進入倒數第二個部分,dovecot的socket 最後是SSL的設定 大功告成! 一樣記得開防火牆的port…

  • 極速快感 飆風再起2 載入畫面當掉

    這次富堅的有點久,來分享一下之前遇到的問題 NFSUG2是一款很好玩的遊戲,它有經典的熱血音樂跟鋼彈合金車,以及很強的小嘍囉配上超廢的最終boss 雖然是XP時代的遊戲,但在Windows 10一樣可以正常執行 唯一會有問題的是,怎麼常常在載入畫面都會卡住,接著遊戲就當掉了 後來找到原因是多核心處理器造成的,那個年代還沒有多核心,想想也是正常的 解決辦法就是調整cpu affinity,讓他只使用一顆核心來執行 有兩種做法,一個是遊戲打開之後去工作管理員設定 另外一種是直接寫cmd檔,在執行的同時就進行設定 有關start的affinity說明可以參考底下這個連結 https://blogs.msdn.microsoft.com/santhoshonline/2011/11/24/how-to-launch-a-process-with-cpu-affinity-set/

  • 在VPN閘道上建立 ipsec only 服務

    最近剛好有在ipsec中修改特定網址DNS的需求,就試著在ipsec server上同時架設這兩種服務 測試用的環境是vultr的VPS,作業系統是Fedora 25,網路介面只有一個ens3 ipsec server的部份則參照之前的文章作設定 此時ipsec連上後,如果將dns server設為ipsec server對外IP時,會發現dns request全部都走在ipsec通道外面,而且dns server必須接收來自網際網路的查詢 這是因為用戶端上的路由表會將ipsec server設成直接連線,再將default gateway改成ipsec出去,所以必須想其他的辦法來解決這個問題 我的方法是在loopback device上建立IP alias,只要新增底下兩個檔案並重開機即可 如此一來,執行ifconfig時可看到loopback裝置已經加上10.10.10.10這個IP 接著再將dns server設定listen-on 10.10.10.10就完成了 如果無法連線,此時要檢查ens3那邊的防火牆是不是有開啟port 53 我一開始也無法理解為什麼會這樣,不過觀察的結果是ipsec流量一定會會發生在ens3上 即便利用了loopback路由,dns request流量還是會從ens3進來,所以防火牆封鎖ens3進來的port時仍然會被阻擋 這部份我解讀成ipsec tunnel在network layer被kernel處理掉了,看起來就像client直接接在ens3一樣 大概是比較習慣OpenVPN那種TUN device的思維,因此這部份研究了比較久 =====2017/07/01更新===== 如果network scripts沒有反應的話,也可以用systemd做startup script 檔案放好之後執行底下的指令,服務就會被放到開機流程裡