今日はQuailyの技術アーキテクチャを解説していきましょう。まずは、いくつかの原則を定めます:
- ベンダーロックインを避ける——Cloudflareにロックインされることも含めて。いつでも Linux ベアメタルに移行できるようにします。
- 依存関係は可能な限り減らす。
- 新しい技術は必要でなければ学ばない。
- 要件を満たせるなら装飾はしない。
- パフォーマンスが最も重要なシーンでのみ実行効率を考慮する。
はい、これで。では始めましょう。
ルーティング
quaily.comを開くと Quaily のホームページが表示されます。これは Cloudflare 上にデプロイされた Hugo サイトです。
ただし、ドメイン quaily.com は直接 Hugo がデプロイされた worker を指すのではなく、トラフィックを振り分ける役割を持つ worker を指します。これをquaily-router
と呼ぶことにしましょう。
この router は、リクエストのパスなどの条件に基づいて、以下のような workers にトラフィックを振り分けます:
routes = ["quaily.com/*"]
services = [
{ binding = "front", service = "front" },
{ binding = "dashboard", service = "dashboard" },
{ binding = "portal", service = "portal" },
{ binding = "tools", service = "tools" },
]
このうち、front と dashboard は Vue のSPA、portalはホームページ、toolsは Tools という手書きの静的サイトです。
同時に、quaily-router
は以下のような静的リソースへのリクエストも処理します:
- 記事ページへのリクエスト(例:
https://quaily.com/lyric_kiji/p/quaily-technology-architecture-explanation
) - 記事一覧ページへのリクエスト(例:
https://quaily.com/lyric_kiji
) - sitemapとfeedへのリクエスト(例:
https://quaily.com/lyric_kiji/sitemap_index.xml
とhttps://quaily.com/lyric_kiji/feed/atom
)
これらの静的リソースはすべて Cloudflare R2 に保存されています。
したがって、メインドメイン quaily.com への完全なリクエストのトラフィックフローは以下のようになります:
quaily-router
はworkerで実装されていますが、必要な場合はnginxのような従来型の方法でも実装可能です。また、R2はS3と互換性があります。そのため、Cloudflareにロックインされることはありません。
フロントエンド
dashboardとfrontはシンプルなVue SPAです。
そのうち、
- dashboardは
quaily.com/dashboard
で動作し、ログイン後のすべてのUIビジネスを担当します。 - frontは記事と記事一覧以外のすべてのページ(例:
quaily.com/lyric/about
など)を担当します。
上記の図から分かるように、記事と記事一覧はSPAでもなく、バックエンドレンダリングの結果でもなく、静的化されたHTMLページです。
この方法の利点は、リクエスト開始からコンテンツ表示までの遅延が小さいことです。quaily-router
がR2から読み出して返すだけでよく、平均遅延は100ms以内で、一般的なコンテンツサイトより3〜5倍速いです。
記事や記事一覧の動的コンテンツ(購読フォームなど)はHTMLにVue SPAをマウントすることで実現します。これらのコンテンツの読み込みには追加の時間が必要ですが、それらが読み込まれる前でも、人間や検索エンジンが記事内容やリストを読むことには影響しません。
バックエンド
フロントエンドはRESTful APIを通じてバックエンドと通信し、エンドポイントはapi.quail.ink
で、ロードバランサーを指しています。ロードバランサーはAWSのデフォルト設定を使用していますが、必要な場合はいつでもnginxに切り替えることができ、ロックインされることはありません。
ロードバランサーの背後には複数のインスタンスがAPIサービスを提供し、バックグラウンドタスクを処理するworkerインスタンスが1台あります。これらは実際にはすべて同じGoで書かれたプログラムで、同じpostgresqlデータベースインスタンスに接続しています。
完全なフロントエンドとバックエンドのアーキテクチャは以下のようになります:
運用
データベースバックアップ
cronスクリプトで実行され、毎日暗号化されたS3にバックアップし、telegramチャンネルに通知を送信します。
多くのビジネス通知がtelegramに送信されます
ログ収集
Quailyのすべてのサービスプロセスはsystemdで実行され、ログはsyslogです。そのため、最も簡単な設定方法はrsyslogを設定し、ログを集中ログサーバーに送信して、/var/log/hosts/HOSTNAME
に保存することです。
ビジネスインスタンスの場合、rsyslog.confを以下のように設定します:
$PreserveFQDN on
*.* @@LOG_SERVER_ADDR:514
$ActionQueueFileName queue
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionQueueType LinkedList
$ActionResumeRetryCount -1
ログインスタンスの場合、rsyslog.confを以下のように設定します:
module(load="imtcp")
input(type="imtcp" port="514")
$AllowedSender TCP, 127.0.0.1, 172.26.0.0/24
template(name="PerHostLog" type="string" string="/var/log/hosts/%HOSTNAME%/%PROGRAMNAME%.log")
*.* action(type="omfile" DynaFile="PerHostLog")
リアルタイムログを観察する必要がある場合は、以下のように統合します:
multitail -cS slog -f /var/log/hosts/quail-0/quail.log -cS slog -I /var/log/hosts/quail-1/quail.log ...
スケールアウト
k8sのような知識を学びたくないし、学ぶつもりもないので、スクリプトを書きました。このスクリプトを実行するだけで、新しいLinuxインスタンス上ですべての設定(systemd、rsyslogなど)を完了できます。
バッチ操作が必要な場合は、zellijのsync mode(ctrl + t, s)を使用します。これにより、1つのタブ内のすべてのpaneに同時に入力できます。
モニタリング
モニタリングにはuptimerobotとsentryを使用しています。
ビジネスアラートにはtelegramを使用しています。
ビルド
ほとんどのビルドはgithub actionで完了します。一部はビルドマシンで完了し、スクリプトでインスタンスにアップロードされます。以下のような感じです:
#! /bin/bash
set -e
declare -a arr=(
"inst-0"
"inst-1"
"inst-2"
# ...
)
echo "📦 build..."
VER=$(git describe --tags --abbrev=0)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o quail -ldflags="-X main.Version=$VER -X ..."
for host in "${arr[@]}"
do
echo "📤 scp to $host"
scp quail $host:/opt/quail/quail-new
echo "🚀 restart..."
ssh $host "cd /opt/quail && mv quail-new quail && sudo systemctl restart quail.service"
echo "🙆 deployed!"
done
したがって、バックエンドと運用関連は以下のようになります:
AI
QuailyはOpenAIやClaude.aiなどのAIサービスを使用しています。AIに関連するビジネスを処理する際には、リトライ、ロードバランシング、フォーマット、タスクルーティング、CoTなど、複雑な問題に頻繁に遭遇します。そのため、この部分の作業は独立したサービスセットに任せています。これには、スケジューラーと複数のproxyが含まれます:
- スケジューラーはAIタスクのスケジューリングを担当します。例えば、どのタスクをどのproxyに割り当てるか、proxyが429エラーを返した場合の再スケジューリングなど。
- proxyはタスクの処理を担当します。例えば、あるproxyは小さなタスクを専門に処理し、4o-miniに割り当てられます。別のproxyはテキスト関連のタスクを担当し、claude-ai-3-5-sonnet-v2に割り当てられます。
大体こんな感じです:
以上がQuailyの技術アーキテクチャです。完全な概要図は以下の通りです: