










サーバー:Oracle Cloud ARM | アプリケーション:Halo 2.x + PostgreSQL 15 + Nginx | レコード時間:2026.05.21–22

私のブログ(wuqishi.com)にはとても厄介な問題がある:毎日20:00から23:00まで正確にフリーズするが、同じサーバー上の他のウェブサイトには何の問題もない。
サーバーはOracle CloudのARMインスタンスで、4コア24GBメモリ、Docker Composeで動かしている。Halo以外にもTypechoのサイトと純粋な静的ページがいくつかある。昼間はすべてのサイトが非常に速いが、Haloは夜になるとまるで魔術にかかれたかのように——ページがロードして回転し、バックエンドにアクセスできず、時折直接エラーが出るERR_CONNECTION_CLOSED。
もっと奇妙なのは、が常にフリーズするのではなく、断続的にフリーズすることです。時には開けることができ、2回押すとフリーズする;時には完全に接続できず、数分後には直る。この「シュレーディンガーのフリーズ」は最も頭を悩ませるものです、なぜなら規則性が全く見つからないからです。
最初はHalo 2.x が Java(Spring Boot)に基づいているのではないかと疑いました。Javaはメモリを食うことで有名なため、TypechoはPHPで使ってすぐに解放されるので、Docker設定を見てJVMのヒープメモリが5G与えられており、G1ガーベッジコレクターも開いており、理論上は問題ないはずでした。
💡 Tip 1:選択的フリーズに遭遇したら、すぐにコードの調整や設定の追加をせずに。
最初のステップは常に問題の範囲を確認することです。。他サーバーの他のサイトは正常 → 全局リソース問題を除外;特定のアプリケーションが遅延 → 問題はおそらくそのアプリケーション自体、そのデータベース、またはそのドメイン専用のトラフィック上にある可能性が高い。
まず Nginx の error.log を確認し、大量のこのようなエラーが表示されることを発見:
2026/05/21 22:14:57 [error] 1234#1234: *45678 recv() failed (104: Connection reset by peer)
2026/05/21 22:14:57 [error] 1234#1234: *45678 upstream prematurely closed connectionそれらはすべて同じパスに指している:
/apis/online-user.zyx2012.cn/v1alpha1/online-wsこれは私が Halo にインストールした "オンラインユーザー数統計" プラグインだ。Nginx がリクエストを Halo にフォワードした後、Halo がレスポンスヘッダーを返す前に接続を切断したように見える。さらに恐ろしいことに、これらのエラーはスクリーンに大量表示されるレベルで、1秒間に10本以上発生する。
を同時に確認した際、二つの現象が見られましたdmesg:
第一に、Docker コンテナが頻繁に再起動しています
vetha1b2c3d4: entered disabled state
vetha1b2c3d4: left promiscuous mode
docker0: port 2(vetha1b2c3d4) entered disabled state仮想ネットワークインターフェースが絶えず削除・再作成されています。これは Halo コンテナがクラッシュ → Docker による自動起動 → またクラッシュというサイクルに陥っていることを意味します
。第二に、UFW ファイアウォールが激しくスキャンを遮断しています
[UFW BLOCK] IN=eth0 OUT= MAC=... SRC=185.191.171.12 DST=... DPT=5432
[UFW BLOCK] IN=eth0 OUT= MAC=... SRC=194.165.16.73 DST=... DPT=5432
画面いっぱいに[UFW BLOCK]が表示され、さまざまな海外 IP アドレスが私のポートをスキャンしています:5432(PostgreSQL)、5985(WinRM)、3389(リモートデスクトップ)…
など、5432?私のデータベースポートが外部に公開されていませんか?
💡 Tip 2:dmesg はシステムレベルの問題を特定するための強力なツールです。
多くの人がアプリケーションログだけを見て、カーネルログを見落とします。Docker コンテナの頻繁な再起動、ネットワークインターフェースの状態の変化、OOM killer によるプロセスの殺害など、これらは dmesg で一望遠くに見えます。dmesg -T | grep -E "(UFW|veth|docker0)" を使って重要な情報を迅速にフィルタリングすることをお勧めします。
私は docker-compose.yml を開いたら、驚きました:
services:
halodb:
image: postgres:15-alpine
ports:
- "5432:5432"当時、DBeaver でデータベースに接続するのを簡単にするために、直接ホストマシンにマッピングしました。しかし、私は Linux 上の大きな落とし穴を忘れていました:Docker は UFW 防火墙を回避します。
ここには多くの人が知らない詳細がある:
UFWは本質的にiptablesをラッピングしており、INPUTチェーンにルールを挿入する。しかし、Dockerはコンテナを起動すると、iptablesのDOCKERチェーン(natテーブルとfilterテーブルに属する)に自分のルールを挿入し、の優先度はUFWのINPUTチェーンよりも高い。
具体的には、Dockerは次のようなDNATルールを作成する:
sudo iptables -t nat -L DOCKER -n --line-numbers | grep 5432DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:5432 to:172.18.0.2:5432このルールの意味は、すべてのホストマシンの5432ポートへのアクセストラフィックが、Dockerコンテナ内の172.18.0.2:5432にDNATされるということです。UFWのフィルタリングルールの前に起こりましただから UFW は全く役に立たない。
データベースのログを見るほど驚くべきことです:
2026-05-21 22:14:57.123 UTC [12345] FATAL: password authentication failed for user "postgres"
2026-05-21 22:14:57.123 UTC [12345] DETAIL: Role "postgres" does not exist.
2026-05-21 22:14:57.456 UTC [12346] FATAL: password authentication failed for user "postgres"
2026-05-21 22:14:57.789 UTC [12347] FATAL: password authentication failed for user "postgres"ハッカーがデフォルトのユーザー名を使っていますpostgresデータベースを暴力破解しようとしている。Docker Composeでデータベースのユーザー名を変更したから、システムは全く認識していないpostgresこのユーザーなので、毎回解錠するのはFATALレベルエラー。
なぜ夜になると特に重いのですか?
夜間はグローバルスキャナーのピーク時(欧米の昼間の業務時間に相当)で、毎秒数十回の悪意のある接続が発生するため。PostgreSQLのデフォルト設定では、各接続ごとに以下の処理が必要です:
バックエンドプロセスをフォークする(約5-10MBのメモリ使用)
SSLハンドシェイクと認証計算を行う
システムテーブルをクエリしてユーザー名を検証
失敗した場合にFATALログを記録
これらの不要なリクエストがCPUとI/Oを圧迫する。私のHaloブログがデータベースを検索したい?待ち行列に入ってください。
ポートをローカルループバックにバインドする:
services:
halodb:
image: postgres:15-alpine
ports:
- "127.0.0.1:5432:5432"
environment:
- POSTGRES_USER=halo
- POSTGRES_PASSWORD=your_strong_password
- POSTGRES_DB=halo変更後リスタートすると、データベースログが瞬時にクリアになり、FATALエラーが完全に消滅する。
💡 Tip 3:すべての機密サービスのDockerポートマッピングは、永遠に以下のように記述する127.0.0.1:PORT:PORT。
は怠って単にPORT:PORTを書かないで、それは公网上の裸で走るのと同じです。UFWはDockerの前では紙の虎です。もし本当に外部からデータベースに接続する必要があるなら、SSHトンネルやWireGuardを使って、直接ポートを公開しないでください。
データベースは静まり返っていますが、ウェブサイトは時折遅延します。ログをさらに確認すると、Nginxのaccess.logに規則があります:ページの読み込みごとに、メインリクエスト以外にも多くの/apis/... のAPIリクエストが走っています。これらのリクエストはすべて
proxy_pass http://127.0.0.1:8090;を通じて行われており、upstreamを経由していません。
問題はここにあります:私はupstream keepaliveを設定していません。
Nginxは毎回リバースプロキシを行うたびに新しいTCPコネクションを新規作成し、使用後は破棄します。夜間に並行処理が高くなると、システム内に数千個のTIME_WAIT状態のコネクションが積み重なります。
ssコマンドで確認できます:
ss -tan state time-wait | wc -l当時、ピーク時にはこの数値が8000+に達しました。
TIME_WAITはTCP四次ハンドシェイクの正常な状態(主動終了側が2MSL待機し、最後のACKが相手側に受信されることを確実にする)ですが、数が多すぎると:
一時ポートが枯渇します:Linux のデフォルトの一時ポート範囲は 32768–60999 で、約 28000 個です。8000+ の TIME_WAIT は利用可能なポートが急減し、新しいリクエストが接続できなくなり、「回転中」「白い画面」のように現れます。
が使用するメモリ:各 TIME_WAIT 接続はカーネル内で約 1–2KB の TCP 制御ブロックを占有し、積み重なることで増加します。
:Nginx の設定の上部で upstream を定義します:
upstream halo_backend {
server 127.0.0.1:8090;
keepalive 64;
} その後、すべての proxy_pass http://127.0.0.1:8090 を proxy_pass http://halo_backend に変更し、対応する location に追加します:
proxy_http_version 1.1;
proxy_set_header Connection "";これにより、Nginx と Halo の間で 64 つの長い接続が再利用され、頻繁な 3-way handshake が不要になります。
2つの詳細に注意してください:
proxy_http_version 1.1 は必須です、なぜなら HTTP/1.0 はデフォルトで keepalive をサポートしないからです。
proxy_set_header Connection "" は、Nginx がクライアントの Connection: close をバックエンドに透過させず、バックエンドが長い接続を主動的に切断するのを防ぐためにあります。
を変更した後、すぐに ss で検証できます:
ss -tan state established '( dport = :8090 or sport = :8090 )' | wc -l接続数は以前の数百個から安定した約64個に急上昇し、それ以上増加しません。TIME_WAIT の数も正常レベル(数十から百個)に低下しました。
💡 Tip 4:ss で netstat を置き換え、より速く正確になります。
特定ポートの接続状態の分布を確認する:
ss -tan state time-wait '( dport = :8090 )' | wc -l
ss -tan state established '( dport = :8090 )' | wc -lもし TIME_WAIT が5000を超える場合、注意が必要です。
今度は「オンライン人数統計」プラグインについて話しましょう。その原理は、フロントエンドがWebSocketの長接続を通じてオンライン状態をリアルタイムで報告することです。
WebSocketは通常のHTTPリクエストではありません。その確立プロセスは以下のようになります:
クライアントはUpgrade: websocketとConnection: Upgradeヘッダーを含むHTTPリクエストを送信します
サーバーは応答101 Switching Protocols、その後HTTPからWebSocketの全二重通信チャネルにアップグレード
、以降のデータはフレーム(frame)で転送され、HTTPのリクエスト/レスポンスモデルを走らなくなります
。問題は第2ステップにあります:、私のNginxがUpgradeヘッダーを正しく転送できていません。
、NginxはデフォルトでUpgradeのような非標準ヘッダーをフィルタリングする(または通常のHTTPリクエストとして扱う)ため、後端のHaloは通常のGETリクエストではなくWebSocketハンドシェイクリクエストを受信します。後端はこれを通常のAPI呼び出しと考えて処理し、接続を閉じます
。しかし、フロントエンドのJSがWebSocketが切断されたと検知すると、自動的に再接続します。、それによって「接続 → 切断 → 再接続」の無限ループが形成されました。
、access.logからこのセットアッププロセスがはっきりと見られます:
22:14:57 "GET /apis/online-user.../online-ws HTTP/1.1" 101 197
22:14:59 "GET /apis/online-user.../online-ws HTTP/1.1" 101 169 ← 同一个 IP,2 秒后重连
22:16:04 "GET /apis/online-user.../online-ws HTTP/1.1" 101 0 ← 传输 0 字节,空连接
22:16:48 "GET /apis/online-user.../online-ws HTTP/1.1" 101 143
22:16:56 "GET /apis/online-user.../online-ws HTTP/1.1" 101 143 ← 8 秒后又重连一つの訪問者でも短時間で3~4回のWebSocketハンドシェイクを発行できます。夜になると数十人の訪問者がいて、様々なスクレイピングツールが加わり、Haloのスレッドプールがこれらの無効なハンドシェイクでいっぱいになり、通常のページリクエストは待ち行列に並び、タイムアウトするまで待たなければなりません。
Nginx error.log の中の Connection reset by peer と prematurely closed connection はこんな風に生まれました——Haloが圧倒され、直接粗暴に接続を切断して自衛しました。
このプラグインをアンインストールしていない理由は、長いリンクを追加した後、現在は問題がないため、ここに記録する:NginxでWebSocketを正しくサポートできる。対応するlocationに追加する:
location /apis/online-user.zyx2012.cn/ {
proxy_pass http://halo_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}検証:
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: wuqishi.com" -H "Origin: https://wuqishi.com" https://wuqishi.com/apis/online-user.zyx2012.cn/v1alpha1/online-ws返信:
HTTP/2 101 Switching Protocols
upgrade: websocket
connection: upgrade
101 Switching Protocolsこれでハンドシェイクが成功し、長い接続が確立されたことを示す。ブラウザのF12のNetworkタブでも、WebSocket接続状態が(pending)から101に変わって、その後緑色の小さな点に変わり、頻繁に切断と再接続されなくなった。
現在このプラグインは非常に安定しており、オンライン人数の統計機能が完全に正常で、以前の再接続の嵐が再び発生していない。
💡 Tip 5:WebSocket の問題を調査する際、ブラウザコンソールのエラーメッセージだけを見ないでください。
curl を使って直接ハンドシェイクリクエストをシミュレートするのが最も速い方法です。もし返答が 200 OK ではなく 101 であれば、Nginx またはバックエンドが Upgrade ヘッダを正しく処理していないことを意味します。また、HTTP/2 が WebSocket に対応する方法は特別なため、Nginx は 1.25.1 より前のバージョンでは HTTP/2 の下での WebSocket をサポートしていません。もしあなたの Nginx のバージョンが古い場合、HTTP/1.1 でテストすることをお勧めします。
調査中、Nginx access.log には、私を笑わせるような別のリクエストの種類もあります:
185.191.171.12 "GET /wp-content/themes/begin/inc/go.php?url=..." 404
85.208.96.196 "GET /wp-content/themes/begin/inc/go.php?url=..." 404SemrushBot(悪名高いSEOスパイダー)が私のウェブサイトの根本不存在的 WordPress 路径を乱暴にリクエストしている。私はもうTypechoを転向させたが、今はHaloに転向している。彼はまだ私がWordPressサイトだと思っていて、Beginテーマのリダイレクトバグ(過去の脆弱性)を探っており、私のサーバーを使って黒帽SEOの踏み台にしようとしている。
これらのリクエストはすべて404で返されるが、問題は:それらはすべてHaloのバックエンドに入っていること。
Nginxが物理ファイルをマッチングできなかったので、@halo_proxyに渡され、Haloは404リクエストごとに完全なページレンダリングを初期化し、404ページを出力しなければならない。1秒に十数件のリクエストがあり、スレッドプールが無駄に使われている。
私はNginxレベルでこれらのスパムボットをブロックしていません。代わりにCloudflareで処理しています。CloudflareのファイアウォールルールはUser-Agentに基づいてSemrushBot、DotBotなどのスパムボットを直接ブロックできるため、サーバーのリソースを消費する必要がありません。
この方法の利点は:
スパムトラフィックはエッジノードでブロックされるため、サーバーに到達する前に阻止されます
Nginx内のスパムボットのブラックリストをメンテナンスする必要がなく、Cloudflareの脅威インテリジェンスデータベースが自動的に更新されます
Nginxのログを汚染せず、access.logには本物の訪問者のみが残ります
💡 Tip 6:Cloudflareを使用している場合は、優先的にエッジノードでスパムトラフィックをブロックしてください。

式コード:
(http.request.uri.path contains "/.env") or
(http.request.uri.path contains "/.git") or
(http.request.uri.path contains "/wp-") or
(http.request.uri.path contains "/xmlrpc.php")Cloudflareのファイアウォールルール、Bot Fight Mode、User-Agentのブロックは、オリジンサーバーのNginxで処理するよりも効率的です。オリジンサーバーのリソースは本物のユーザーに割り当てるべきで、クローラーへの応答に無駄に使うべきではありません。もしあなたのサイトがWordPressでない場合、Cloudflareでwp-content、wp-adminを含むすべてのリクエストをブロックできます。404を返す必要すらありません。
/rss.xmlは毎回115KBのXMLを動的に生成し、FreshRSS、Inoreader、AstraHubなどの複数のRSSアグリゲーターが数分ごとにクロールしてきます。現在、静的キャッシュは作成していません。暫くおきます。
💡 Tip 7:RSSアグリゲーターのクロール間隔は異なります。
5分ごとにクロールするものもあれば、30分ごとにクロールするものもあります。あなたはaccess.log ユーザーエージェントを分析し、最も頻繁にアクセスするアグリゲータを特定して、ターゲットに合わせたキャッシュ時間を設定します。また、HaloのRSS生成は全量の記事リストであり、記事が多く(数百件)の場合、毎回生成する際にO(n)の反復処理が必要で、CPUを非常に消費します。ページ分割されたRSSや出力数を制限することを検討することをお勧めします。
ログに大量のこのようなリクエストが見つかりました:
GET /apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https://cdn.ssslove.com/...&size=m HTTP/2.0" 302 0Haloのサムネイルサービスは外部リンクの画像に対して302リダイレクトを行います。ページ内の画像が多い場合、この往復で多くの無駄なリクエストが増加します。今後は、画像リンクをCDNパラメータ付きの直リンクに置き換え、Haloのプロキシ層をスキップすることを検討します。
💡 Tip 8:Haloのサムネイルプロキシは、ローカルにアップロードされた画像を処理するのに適しています。
外部リンクの画像(例えば又拍云、OSS)の場合は、フロントエンドでCDNのサムネイルパラメータ(例えば !m )を使用することをお勧めします。?x-oss-process=image/resize),Halo の代理層をスキップし、302 ルーティングを1回減らす。
Docker ComposeでHALO_EXTERNAL_URLがhttp://localhost:8090/,这会导致某些插件在生成回调地址、RSSリンクで誤ったドメイン名を使用しています。
実際のドメイン名に変更すべきです:
environment:
- HALO_EXTERNAL_URL=https://wuqishi.com/💡 ヒント9:HALO_EXTERNAL_URLは、ページリンクだけに影響するわけではありません。
それも影響します:
Open Graph ラベル(WeChatやTwitterで共有する際のプレビューカード)
RSS で<リンク> と [atom:link](atom:link) 要素
一部のプラグインのOAuthコールバックアドレス
メール通知内のリンク
間違えていたことで、共有された記事のリンクが http://localhost:8090/...,别人根本打不开。現在、私はまだ修正していない。
今回の調査では少し道を外れたが、効果的な方法論を検証することができた:
調査の順序 | 何をした | 何を発見した | 重要なコマンド / ツール |
|---|---|---|---|
範囲の確認 | 他のサーバー上の他のサイトと比較 | Halo カードのみで、グローバルリソースの問題を排除 | |
2. Nginx の error.log を確認 | フロントエンドとバックエンドの切断原因を特定 | 多数の |
|
3. dmesg を確認 | システムレベルの例外をチェック | Docker の頻繁な再起動 + UFW の大量の遮断 |
|
データベースログを確認 | DBの負荷をチェック | 公网での暴力攻撃、FATALメッセージで画面が乱れる |
|
5. Nginx access.logを確認 | リクエストの分布を分析 | 悪意のあるスクレイピング + WebSocketの高頻度再接続 | |
6. ブラウザのF12を開く | フロントエンド/バックエンドの遅延を区別 | TTFBが高く、バックエンドの応答が遅いことを確認 | Chromeデベロッパーツール → ネットワーク → 時間 |
7. 接続状態を確認 | TCPリソース枯渇を確認 | TIME_WAIT 8000+ |
|
8. WebSocketハンドシェイクのシミュレーション | Upgradeヘッダ転送の検証 | 200を返す代わりに101を返す | |
核心原則:外から内へ、まず不要なトラフィックを遮断し、次に内部設定を修正する。
最初からJVMパラメータを調整したり、データベースインデックスを追加したりすると、真の原因を見つけることが永远にできないかもしれない。根本的な問題はパフォーマンス不足ではなく、不要なリクエストによってリソースが無駄に消費されている。
設定を変更し、サービスを再起動した後、今日までログを確認して30分間過ごした:
データベースログ:静まり返り、時折通常のクエリインタラクション
Nginx error.log:ゼロのエラー報告
Nginx access.log:多くがクリーンになり、Cloudflare が大部分のスパムクローラーをブロックしている
WebSocket プラグイン:ハンドシェイクで 101 Switching Protocols 返り、長期接続は安定し、頻繁な再接続がなくなった。オンライン人数統計機能は正常に作動している
ページの読み込み:iPad、Mac、スマートフォン 4G テストで、通常通りに開ける
夜間 22:30 から 23:00 という本来最も混雑する時間帯が、今では昼間と同じくらいスムーズです。(今夜も引き続き確認してみます)
データベース、Redis、MQ などの機密サービスは必ずバインド127.0.0.1してください。UFW を信じてはいけません。信じるべきものはiptables -t nat -L DOCKERです。
高負荷時には必ずkeepaliveを有効にしてください。そうしないとTIME_WAITがあなたを壊します。ss -tan state time-wait | wc -l 5000を超えると注意が必要です。
UpgradeとConnectionヘッダーはどちらも不可欠です。curlを使ってハンドシェイクをシミュレートして迅速に検証し、101が返ってこないと成功とは言えません。また、Nginxのバージョンにも注意が必要です。HTTP/2でのWebSocketにはNginx ≥ 1.25.1が必要です。
今回のオンライン人数プラグインの問題の根源はプラグイン自体ではなく、Nginxの設定不足にある。修正後、プラグインは非常に安定して動作し、機能も完全に正常である。したがって、プラグイン関連の問題が発生した場合は、まずインフラストラクチャ(Nginx、リバースプロキシ、ファイアウォール)を確認し、急いでプラグインをアンインストールしないようにする。
今回の調査ではJavaコードを一行も変更せず、すべての問題はログから特定した。ログを確認する習慣をつけることは、無目的なパラメータ調整よりも重要である。
Nginxのログにこれらのフィールドを追加することを推奨する。これにより、前段が遅いか後段が遅いかをすぐに判断できる:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'rt=$request_time urt=$upstream_response_time';rt(request_time):クライアント視点での総時間
urt(upstream_response_time):後段の処理時間
もし rt が大きい場合はurt が小さい → 問題はネットワークまたは Nginx にあります;もし urt が大きい → 問題はバックエンドです。
SemrushBot、DotBot などの商用クローラは非常に aggressive で、あなたの robots.txt を遵守しません。また、WordPress の脆弱性のパスを狙ってスキャンします。もしあなたのサイトが WP でない場合、Cloudflare または Nginx レイヤーで直接ブロックし、バックエンドのリソースを無駄にしないでください。
もし Halo や似たような Java ブログシステムを使っていても夜間にパフォーマンスが低下する場合、この記録があなたにいくつかの調査のアプローチを与えることを願っています。時には問題はコードの複雑さではなく、ある設定の詳細、あるプラグイン、または公開されているポートにあることがあります。
最後に一言:SemrushBot は本当に執着しており、21:00 から 23:00 まで無停電で2時間もスキャンし続け、Cloudflare に何百回もブロックされた後も試し続けた。このような職務熱心さは、正しい道に使ってあればもうアーキテクトになっていただろう。
2026年5月22日未明に記載、一部の内容はAIによる検索後に整理されたものです。
このコンテンツは慣性聚合(RSSリーダー)によって自動集約されています。参考としてご覧ください。 原文出典 — 著作権は原著者に帰属します。