今日はQuailyの技術アーキテクチャを解説していきましょう。まずは、いくつかの原則を定めます:

  1. ベンダーロックインを避ける——Cloudflareにロックインされることも含めて。いつでも Linux ベアメタルに移行できるようにします。
  2. 依存関係は可能な限り減らす。
  3. 新しい技術は必要でなければ学ばない。
  4. 要件を満たせるなら装飾はしない。
  5. パフォーマンスが最も重要なシーンでのみ実行効率を考慮する。

はい、これで。では始めましょう。

ルーティング

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.xmlhttps://quaily.com/lyric_kiji/feed/atom

これらの静的リソースはすべて Cloudflare R2 に保存されています。

したがって、メインドメイン quaily.com への完全なリクエストのトラフィックフローは以下のようになります:

An image to describe post

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データベースインスタンスに接続しています。

完全なフロントエンドとバックエンドのアーキテクチャは以下のようになります:

An image to describe post

運用

データベースバックアップ

cronスクリプトで実行され、毎日暗号化されたS3にバックアップし、telegramチャンネルに通知を送信します。

An image to describe post

多くのビジネス通知が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に同時に入力できます。

モニタリング

モニタリングにはuptimerobotsentryを使用しています。

ビジネスアラートには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

したがって、バックエンドと運用関連は以下のようになります:

An image to describe post

AI

QuailyはOpenAIやClaude.aiなどのAIサービスを使用しています。AIに関連するビジネスを処理する際には、リトライ、ロードバランシング、フォーマット、タスクルーティング、CoTなど、複雑な問題に頻繁に遭遇します。そのため、この部分の作業は独立したサービスセットに任せています。これには、スケジューラーと複数のproxyが含まれます:

  • スケジューラーはAIタスクのスケジューリングを担当します。例えば、どのタスクをどのproxyに割り当てるか、proxyが429エラーを返した場合の再スケジューリングなど。
  • proxyはタスクの処理を担当します。例えば、あるproxyは小さなタスクを専門に処理し、4o-miniに割り当てられます。別のproxyはテキスト関連のタスクを担当し、claude-ai-3-5-sonnet-v2に割り当てられます。

大体こんな感じです:

An image to describe post


以上がQuailyの技術アーキテクチャです。完全な概要図は以下の通りです:

An image to describe post

カバー画像のソース