動かざることバグの如し

近づきたいよ 君の理想に

Node.js TypeScriptサーバーをPM2で永続化する

環境

  • Node.js v24
  • PM2

やりたいこと

TypeScript製のWebサーバーを以下のコマンドで起動している。

node --import tsx ./src/web/server.ts --port 3000

今はこれを nohup で動かしているのだが、いくつか不満がある。

  • サーバー再起動後に自動起動しない(手動で立ち上げ直す必要がある)
  • プロセスが落ちても自動で再起動しない
  • ログ管理が貧弱でローテーションも自分でやる必要がある

これを PM2 で管理することで、プロセスの永続化・自動起動・ログ管理をまとめて解決したい。

解決策

PM2 の設定は ecosystem.config.js というファイルにまとめるのが定番だ。コマンドライン引数が長くなりがちな場合は特にこの形式が便利で、バージョン管理にも乗せやすい。

プロジェクトルートに以下の内容で作成する。

module.exports = {
  apps: [{
    name: 'my-app',
    script: 'node',
    args: '--import tsx ./src/web/server.ts --port 3000',
    cwd: '/home/thr3a/work/my-app',
    watch: false,
    restart_delay: 3000,
  }]
}

起動

pm2 start ecosystem.config.js

サーバー再起動後も自動起動させるには、初回だけ以下を実行する。

pm2 startup を実行するとsystemdのユニットファイルを登録するコマンドが表示されるので、それをコピーして実行する。pm2 save は現在のプロセス一覧を保存し、次回起動時に復元する設定だ。

pm2 save
pm2 startup

よく使うコマンドは以下

pm2 logs my-app # ログ確認
pm2 status # 状態確認
pm2 restart my-app
pm2 stop my-app

参考リンク

LinuxでSSDの型番(モデル名)を確認するコマンド

環境

  • Ubuntu

やりたいこと

Linuxで今接続されているSSDディスクの型番をssh越しで確認したい

コマンド

lsblk -io NAME,TYPE,SIZE,MODEL,SERIAL

試すとこんな感じ

❯ lsblk -io NAME,TYPE,SIZE,MODEL,SERIAL
NAME        TYPE   SIZE MODEL                          SERIAL
nvme0n1     disk 465.8G Samsung SSD 970 EVO Plus 500GB S4EVNF0M693956L
|-nvme0n1p1 part     1G
`-nvme0n1p2 part 464.7G

「Samsung SSD 970 EVO Plus 500GB」でシリアルIDが「S4EVNF0M693956L」というのがわかる

おそらくlsblkコマンドは/sys/blockから取ってるので同じだろうけど以下のコマンドでも確認できる

❯ cat /sys/block/nvme0n1/device/model
Samsung SSD 970 EVO Plus 500GB
❯ lspci | grep SSD
01:00.0 Non-Volatile memory controller: Samsung Electronics Co Ltd NVMe SSD Controller SM981/PM981/PM983

参考リンク

Rubyで100刻みの配列を作る方法

やりたいこと

Rubyで100〜5000の配列を作成したい。しかもただの配列ではなく100刻みでほしい。

コード

最初はmapメソッドで考えて以下で実行してみた。

(1..50).map { |i| i * 100 }

Rubyならもっと便利なビルドインメソッドがあるんじゃないかと思ってAIに聞いてみたら一発でドンピシャな回答をくれた。

(100..5000).step(100).to_a

実行してみるとちゃんと100刻みの100~5000の配列になっている。

irb(main):001> (100..5000).step(100).to_a
=> 
[100,
 200,
 300,
 400,
 500,
 (省略)
  4600,
 4700,
 4800,
 4900,
 5000]

やったね

こういうのって昔はよくググってたんだけどAIがやってくれるから検索流入なくなるんだろうなぁ

参考リンク

NginxでIPブロックしつつ特定のUser-Agentだけ許可する設定

環境

  • Nginx 1.31.1

やりたいこと

以下のようなNginx設定があったとする。

server {
  listen 443 ssl;
  server_name example.com;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-SSL-Cipher $ssl_cipher;

  location / {
    proxy_pass http://myserver;
  }
}

がしかしBot攻撃が激しいのでNginxレベルでIPをブロックしたい。

その場合は以下のようになる。

location / {
  proxy_pass http://myserver;
  include /etc/nginx/block_ips.conf;
  deny all;
}

ただ、このIP一覧にGoogleBot(本物)も含まれてしまっているのでユーザーエージェントに「Googlebot」の文字があれば許可したい。

コード

まず http ディレクティブの中に map ブロックを追加して、$is_googlebot という変数を定義する。User-Agentに「Googlebot」が含まれていれば1、それ以外は0になる。

# httpディレクティブ
map $http_user_agent $is_googlebot {
    default     0;
    ~*Googlebot 1;
}

次に server ブロック側でその変数を使う。if ($is_googlebot) でGooglebotと判定された場合は、内部ロケーション /_googlebot_proxy にリライトして通す。そうでない場合はIPブロックリストの評価に進み、deny all でブロックされる。

/_googlebot_proxyinternal を付けているので外部から直接アクセスはできない。

server {
  listen 443 ssl;
  server_name example.com;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-SSL-Cipher $ssl_cipher;

  location / {
    if ($is_googlebot) {
      rewrite ^ /_googlebot_proxy last;
    }
    proxy_pass http://myserver;
    include /etc/nginx/block_ips.conf;
    deny all;
  }

  location /_googlebot_proxy {
    internal;
    proxy_pass http://myserver$request_uri;
  }
}

MySQLのCould not increase number of max_open_filesエラーとLimitNOFILE

環境

  • MySQL 8.0

発端

journalctl -u mysqld でログを眺めていたら、以下の警告が繰り返し出ているのに気づいた。

[Warning] Could not increase number of max_open_files to more than 65536 (request: 100005)
[Warning] Could not increase number of max_open_files to more than 65536 (request: 100005)

「65536以上に引き上げられなかった」という警告で、放置するとアクセス増加時に Too many open files エラーが発生してMySQLが停止するリスクがある。

65536の数値は自分が設定したもので、デフォルトの1024だと少なすぎるのでsystemdの設定を変更してMySQLのLimitNOFILEを65536に指定していた。

[Service]
LimitNOFILE=65536

また my.cnf でも65535を指定していた。

max_allowed_packet=64MB
open_files_limit=65535
key_cache_segments=32

65536でも足りないのか。。?あと100005という中途半端な値を要求している点はなんだ?

解決策

原因はMySQLとsystemdの設定値の乖離である。 MySQLが 100005 のファイルディスクリプタを必要としているのに対し、systemdの LimitNOFILE のデフォルト上限が 65536 に制限されているため、要求が通らずに警告となっている。

systemd 側の制限を引き上げれば解消できる。

systemctl edit mysqld

オーバーライド用のファイルが開くので、以下を追記して保存する。

[Service]
LimitNOFILE=100005

保存後、設定を反映させる。

systemctl daemon-reload
systemctl restart mysqld

再起動後に journalctl -u mysqld を確認して警告が出なくなっていれば完了だ。

ソースは?

my.cnfopen_files_limit=65535 と書いているのに、なぜMySQLは request: 100005 という値を要求したのか。

MySQLは open_files_limit の設定値をそのまま使うわけではない。起動時に以下の計算を行い、最大の値をOSに要求する仕様になっている。

  • 10 + max_connections + (table_open_cache × 2)
  • max_connections × 5
  • OSの制限値(正の値の場合)
  • 起動時に open_files_limit の指定がない場合は 5000

これはMySQL 8.4の公式リファレンスマニュアルの open_files_limit の項目に明記されている。

dev.mysql.com

The effective open_files_limit value is based on the value specified at system startup (if any) and the values of max_connections and table_open_cache, using these formulas:
- 10 + max_connections + (table_open_cache * 2)
- max_connections * 5
- operating system limit (if positive)
- if operating system limit is infinity: open_files_limit value specified at startup, 5000 if none
The server attempts to obtain the number of file descriptors using the maximum of those four values.

今回の 100005 という値も、max_connections が 20000 前後に設定されていたとすれば 20000 × 5 = 100000 となり、内部的なオーバーヘッドを加えて 100005 が算出される。つまり my.cnfopen_files_limit 設定が計算結果を下回ったため、MySQLが自動的に大きい方の値をOSに要求したわけだ。