利用 systemd 在 Ubuntu 執行 Sidekiq 6
Tags: rails, sidekiq, systemd
Published: 2021-08-20 17:02:56

自從有了 Sidekiq 後，所有事情都變得容易了。
所有需要長時間執行的程式，我全往 sidekiq 扔，我再也不用怕 CloudFlare 的 524(請求超時) 了(咦?)
在 production 的環境執行 sidekiq 雖不困難但對於 Linux 沒那麼熟的人來說還滿多細節要注意的，
Sidekiq 6 又好像跟之前變化頗大。
這次想分享一下將 Sidekiq 6 運行在 ubuntu 20.04 的經驗 : )

## 目標
* 運行 Sidekiq (廢話...)
* 重開機或 sidekiq 掛了的話， sidekiq 會自動重啟
* 當我用 Capistrano 部署新的程式碼時，sidekiq 會讀新的程式碼並重啟

## 我們怎麼執行 Sidekiq?
要知道在 production 環境怎麼運行 sidekiq, 首先要知道在 development 環境是怎麼跑的：
```bash
bundle exec sidekiq
```
其實就那麼容易... 你可以再加一些參數像 `-C xxxx.yml` 去指定要讀取的設定檔。不過我們先維持這樣

基本上你在 production 環境可以做一樣的事，如果你想讓它更像一個”真的服務”，也可以直接加個 “&” 在後面讓它跑在背景:
```bash
RAILS_ENV=production bundle exec sidekiq &
```
如果真的純粹只是想要在 production 環境跑 sidekiq ，其實這樣就夠了

但光這樣做好像沒符合我們的目標：
1. 重開機或程式掛掉後sidekiq不會自動運行
2. 部署完後也不會自動運行
3. 最重要的是，這感覺好像不是很”Pro”。即使我們不是用 Sidekiq Pro , 應該也可以 Pro 一點

好，為了達到我們的目標，我們需要用 `systemd`

## systemd是什麼?
`systemd` 是 Linux 系統專門來管理各式「服務程式」的程式，其實就是 daemon 所以才是 systemD。比如 mysql, apache, nginx, redis 這些都可以用它來管，事實上 `systemd` 是多數 Linux 版本預設的 Service Manager。
有 `systemd`,，我們可以：
* 用 `systemctl start/stop/restart` 任何服務
* 可以啟用(**enable**)服務， 啟用的服務在系統重開時會自動開始運行
* 你可以指定當程式掛掉後，該做什麼事，例如重啟該服務

`systemd` 的內容還有一堆，不過目前知道這樣就足夠了，剩下的就自行 google 吧xD
看起來 systemd 可以符合我們想做的事，就用 systemd 來操作 sidekiq 吧！

## 將 Sidekiq 變成一個服務單元(service unit)
在 systemd 中，每個服務都被視為一個「單元」(Unit)
要新增一個 sidekiq 的服務單元，我們可以新增一個檔案 `/lib/systemd/system/sidekiq.service`
(另外，用來部署 rails 的使用者叫 `deployer`)
```bash
# /lib/systemd/system/sidekiq.service
# 我們的 service 叫 sidekiq
[Unit]
Description=sidekiq
After=syslog.target network.target

# 這個 Type=simple 只是 systemd 要如何判斷你的服務成功執行
[Service]
Type=simple
WorkingDirectory=/path/to/your/app

# 如果是用 rbenv:
# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production'
# 如果直接用系統安裝的 ruby:
# ExecStart=/usr/local/bin/bundle exec sidekiq -e production
# 如果是用 rvm ，用 ruby 2.6.5 也無特定 gemset
# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production
# 如果是用 rvm ，用 ruby 2.6.5 且有特定 gemset
# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5@gemset-name/wrappers/bundle exec sidekiq -e production
# 如果是用 rvm ，用而且專案有 .ruby-version 指定版本
ExecStart=/home/deploy/.rvm/bin/rvm in /path/to/your/app/current do bundle exec sidekiq -e production

User=deployer
Group=deployer
UMask=0002

# 這行可以大大降低 Ruby memory 用量
# 我也是抄來放上
# 不過有看 MALLOC_ARENA_MAX=2 的意思是限制 sidekiq 只能用2個 tread_pool 
# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/
Environment=MALLOC_ARENA_MAX=2

# 如果掛掉就重啟
RestartSec=1
Restart=on-failure

# log 會記在 /var/log/syslog
StandardOutput=syslog
StandardError=syslog

# 這個服務的id 是 sidekiq
SyslogIdentifier=sidekiq

[Install]
WantedBy=multi-user.target
```

 `systemd` 會去找 `/lib/systemd/system/`下所有的檔案,所以 `sidekiq.service` 已經可以被 `systemd` 找到了。
我們接著可以執行下列指令
```bash
# 重讀所有服務
sudo systemctl daemon-reload
# enable sidekiq.service 所以它可以在開機後自動運行
sudo systemctl enable sidekiq.service
# 啟動 sidekiq
sudo service sidekiq start
# 在 /var/log/syslog 看有沒有 sidekiq 的 log
sudo cat /var/log/syslog
# 可以用 ps 看 sidekiq 執行了或
sudo ps aux | grep sidekiq
# 或直接用 systemctl 來看目前運行中的服務
sudo systemctl status
```

## 讓 Capistrano 重啟 Sidekiq
我用 Capistrano 來做自動部署以避免錯誤。
部署完當然希望 sidekiq 重啟，這樣它才讀到最新的程式碼。
要把重啟sidekiq 加到 Capistrano 的流程中，其實只要安裝 gem `capistrano-sidekiq` 就可以了。
在 Gemfile 加入：
```ruby
# Gemfile
gem 'capistrano-sidekiq', group: :development
```
再到 Capfile 加入:
```ruby
# Capfile
require 'capistrano/sidekiq'
# 加入 sidekiq 的 rake tasks
install_plugin Capistrano::Sidekiq
# 設定要用 systemd 去控制 sidekiq
install_plugin Capistrano::Sidekiq::Systemd
```
這樣設定好， `cap production deploy` 時就會依序執行下列工作：
* 停止從 redis 拿工作
* 停止 Sidekiq 服務
* 開啟 Sidekiq 服務

## 做一個一般使用者的 sidekiq.service
上面其實我故意漏說了一件事。
上面新增的 service unit 其實是全系統範圍的，也就是要用 `sudo` 去執行 `systemctl`的
如果我們希望一般的使用者也可以使用 systemd 的話，我們必須要做一個使用者自己的 sidekiq.service
而且 `capistrano/sidekiq` 其實預設是要用一般使用者的權限去執行 systemctl 來重啟 Sidekiq 的

當然我們也可以去改 `capistrano/sidekiq` 的設定，讓它用root 的權限去操作 systemctl，我只是想說明其實有多種選擇，每種都可以。 而且一開始我的 `sidekiq.service` 怎麼都跑不起來就是因為我沒注意到這件事....所以想特別提一下。

If you have already enabled the system-wise `sidekiq.service`, you need to disable it and delete the service-unit:
如果你剛剛已經安裝好了全系統級別的 sidekiq.servive，你需要執行下面步驟去取消它：
```bash
# 先停止 sidekiq
sudo systemctl stop sidekiq
# 不要啟用 sidekiq.service
sudo systemctl disable sidekiq.service
# 刪掉
sudo rm /lib/systemd/system/sidekiq.service
```

一般使用者級別的服務單元要放在 `~/.config/systemd/user/` 下，systemd 會去檢查下面的檔案。我們再新增一次 `sidekiq.service`，它稍有一些不同：
```bash
[Unit]
Description=sidekiq
After=syslog.target network.target

[Service]
# 如果是用 Sidekiq 6.0.6 以後的版本，可以改成 notify 並配合 WatchdogSec
# 否則沿用 simple
Type=notify
WatchdogSec=10

WorkingDirectory=/path/to/your/app/current

# 如果是用 rbenv:
# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production'
# 如果直接用系統安裝的 ruby:
# ExecStart=/usr/local/bin/bundle exec sidekiq -e production
# 如果是用 rvm ，用 ruby 2.6.5 也無特定 gemset
# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production
# 如果是用 rvm ，用 ruby 2.6.5 且有特定 gemset
# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5@gemset-name/wrappers/bundle exec sidekiq -e production
# 如果是用 rvm ，用而且專案有 .ruby-version 指定版本
ExecStart=/home/deploy/.rvm/bin/rvm in /path/to/your/app/current do bundle exec sidekiq -e production
ExecReload=/usr/bin/kill -TSTP $MAINPID

# 這行可以大大降低 Ruby memory 用量
# 我也是抄來放上
# 不過有看 MALLOC_ARENA_MAX=2 的意思是限制 sidekiq 只能用2個 tread_pool 
# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/
Environment=MALLOC_ARENA_MAX=2

# 如果掛掉就重啟
RestartSec=1
Restart=on-failure

# log 會記在 /var/log/syslog
StandardOutput=syslog
StandardError=syslog

# 這個服務的id 是 sidekiq
SyslogIdentifier=sidekiq

# 注意這邊是 default.target
[Install]
WantedBy=default.target
```

我們再跑以下指令來啟用/啟動 `sidekiq.service`:
```bash
systemctl --user daemon-reload 
systemctl --user enable sidekiq.service

# 現在可以控制 sidekiq 啦
systemctl --user {start,stop,restart} sidekiq
```

現在 `capistrano/sidekiq` 應該可以正常運作了，用 capistrano 部署看看吧 : )

## 其它問題

### 用 user-wise systemd 啟動 Sidekiq 但它好像有啟動一下但又停止運作了
這其實是我自己遇到的一個問題，我發現怎麼Sidekiq 的 queue 好長都沒有輪詢(toll)？
但我一登入用 `ps aux | grep sidekiq` 查看 sidekiq 又是正常執行中，佇列中工作也突然開始消化，我當下還跟客戶笑稱這是「薛丁格的隊列」：看它時才會做，不看時不會做...。
搞半天原來 user-wise systemd 只允許「在線上」的使用者執行服務...，當最後一個session 結束時，那些服務也跟著停止。
所以一般如果一台主機就只有一個 rails 服務時，應該是不用 user-wise 去執行的 systemd...用system-wise 的即可。
不過有 systemd 有提供一個指令 loginctl 來控制用戶登錄的狀態，用下面的指令即可保持使用者登入的狀態囉：
```bash
loginctl enable-linger [user_name]
```

### 部署時跑 sidekiq:quiet 出現以下錯誤
```
sidekiq:quiet
    01 systemctl --user reload sidekiq
    01 Failed to reload sidekiq.service: Job type reload is not applicable for unit sidekiq.service.
  ✘ 01 deployer@xxxxxxxx 0.067s
```
解法
	* 加  `ExecReload=/bin/kill -TSTP $MAINPID` 進 sidekiq.service
### 出現 target 找不到
	* target 其實是一組 service unit，執行 target 時全部服務會一起執行
	* 用 `systemctl list-units —type=target` 去看能用的 target
### -L log/sidekiq.log 無效
	* Sidekiq 6 不再支援 log 的轉向，請看 [Logging · mperham/sidekiq Wiki · GitHub](https://github.com/mperham/sidekiq/wiki/Logging)，一律看 /var/log/syslog
	* 可以用 `bundle exec sidekiq 2>&1 | logger -t sidekiq` 為 log 加上 sidekiq 的 tag


## 參考資料
* 其實 `sidekiq.service` 大部分我也是從 sidekiq repo 看來的啦: [sidekiq/sidekiq.service at master · mperham/sidekiq · GitHub](https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service)
* [GitHub - seuros/capistrano-sidekiq: Sidekiq integration for Capistrano](https://github.com/seuros/capistrano-sidekiq)