Loadcatを使用した簡略化されたNGINX負荷分散

公開: 2022-03-11

水平方向にスケーラブルになるように設計されたWebアプリケーションでは、多くの場合、1つ以上の負荷分散ノードが必要です。 それらの主な目的は、着信トラフィックを利用可能なWebサーバー全体に公平に分散させることです。 ノードの数を増やし、ロードバランサーをこの変更に適応させるだけで、Webアプリケーションの全体的な容量を増やす機能は、本番環境で非常に役立つことがわかります。

NGINXは、他の多くの機能の中でも、高性能の負荷分散機能を提供するWebサーバーです。 これらの機能の一部は、サブスクリプションモデルの一部としてのみ利用できますが、無料のオープンソースバージョンは依然として非常に機能が豊富で、箱から出して最も重要な負荷分散機能が付属しています。

Loadcatを使用した簡略化されたNGINX負荷分散

Loadcatを使用した簡略化されたNGINX負荷分散
つぶやき

このチュートリアルでは、ロードバランサーとして機能するようにNGINXインスタンスをオンザフライで構成できる実験ツールの内部メカニズムを探り、きちんとしたWebを提供することでNGINX構成ファイルの本質的な詳細をすべて抽象化します。ベースのユーザーインターフェイス。 この記事の目的は、そのようなツールの作成を開始するのがいかに簡単かを示すことです。 プロジェクトLoadcatはLinodeのNodeBalancersに大きく影響を受けていることは言及する価値があります。

NGINX、サーバーおよびアップストリーム

NGINXの最も一般的な使用法の1つは、クライアントからWebサーバーアプリケーションへの要求のリバースプロキシです。 Node.jsやGoなどのプログラミング言語で開発されたWebアプリケーションは、自給自足のWebサーバーですが、実際のサーバーアプリケーションの前にリバースプロキシを配置すると、多くの利点があります。 NGINX構成ファイルのこのような単純なユースケースの「サーバー」ブロックは、次のようになります。

 server { listen 80; server_name example.com; location / { proxy_pass http://192.168.0.51:5000; } }

これにより、NGINXはexample.comを指すすべてのリクエストをポート80でリッスンし、それぞれを192.168.0.51:5000で実行されているWebサーバーアプリケーションに渡します。 Webアプリケーションサーバーがローカルで実行されている場合は、ここでループバックIPアドレス127.0.0.1を使用することもできます。 上記のスニペットには、リバースプロキシ構成でよく使用される明らかな調整がいくつか欠けていることに注意してください。ただし、簡潔にするために、このように維持されています。

しかし、同じWebアプリケーションサーバーの2つのインスタンス間ですべての着信要求のバランスを取りたい場合はどうでしょうか。 ここで「アップストリーム」ディレクティブが役立ちます。 NGINXでは、「アップストリーム」ディレクティブを使用して、NGINXがすべての着信リクエストのバランスを取る複数のバックエンドノードを定義できます。 例えば:

 upstream nodes { server 192.168.0.51:5000; server 192.168.0.52:5000; } server { listen 80; server_name example.com; location / { proxy_pass http://nodes; } }

2台のサーバーで構成される「ノード」という名前の「アップストリーム」ブロックをどのように定義したかに注目してください。 各サーバーは、リッスンしているIPアドレスとポート番号によって識別されます。 これにより、NGINXは最も単純な形式のロードバランサーになります。 デフォルトでは、NGINXはラウンドロビン方式で着信リクエストを配信します。最初のリクエストは最初のサーバーにプロキシされ、2番目のリクエストは2番目のサーバーにプロキシされ、3番目のリクエストは最初のサーバーにプロキシされます。

ただし、負荷分散に関しては、NGINXにはさらに多くの機能があります。 これにより、各サーバーの重みを定義したり、一時的に使用不可としてマークしたり、別のバランシングアルゴリズムを選択したりできます(たとえば、クライアントのIPハッシュに基づいて機能するアルゴリズムがあります)。これらの機能と構成ディレクティブはすべてnginx.orgで適切に文書化されています。 。 さらに、NGINXを使用すると、ほとんど中断することなく、構成ファイルをオンザフライで変更および再ロードできます。

NGINXの構成可能性とシンプルな構成ファイルにより、NGINXを多くのニーズに簡単に適応させることができます。 また、NGINXをロードバランサーとして構成する方法を正確に説明するチュートリアルがインターネット上にすでに多数存在します。

Loadcat:NGINX構成ツール

プログラムには、自分で何かをする代わりに、他のツールを構成してそれを実行するという魅力的なものがあります。 それらは、おそらくユーザー入力を受け取り、いくつかのファイルを生成する以外には、実際には多くのことを行いません。 これらのツールから得られるメリットのほとんどは、実際には他のツールの機能です。 しかし、彼らは確かに人生を楽にします。 自分のプロジェクトの1つにロードバランサーをセットアップしようとしているときに、NGINXとその負荷分散機能に似たようなことをしてみませんか?

ロードキャットが誕生しました!

Goで構築されたLoadcatは、まだ初期段階にあります。 現時点では、このツールを使用すると、負荷分散とSSLターミネーションのみを目的としてNGINXを構成できます。 これは、ユーザーにシンプルなWebベースのGUIを提供します。 ツールの個々の機能をウォークスルーする代わりに、下にあるものを覗いてみましょう。 ただし、誰かがNGINX構成ファイルを手作業で操作することを楽しんでいる場合、そのようなツールにはほとんど価値がない可能性があることに注意してください。

このためのプログラミング言語としてGoを選択する理由はいくつかあります。 それらの1つは、Goがコンパイル済みバイナリを生成することです。 これにより、依存関係の解決を心配することなく、Loadcatをコンパイル済みのバイナリとしてビルドし、リモートサーバーに配布またはデプロイできます。 セットアッププロセスを大幅に簡素化するもの。 もちろん、バイナリはNGINXがすでにインストールされており、systemdユニットファイルが存在することを前提としています。

Goエンジニアでない場合でも、心配する必要はありません。 Goは、始めるのがとても簡単で楽しいです。 さらに、実装自体は非常に単純であり、簡単に実行できるはずです。

構造

Goビルドツールは、アプリケーションを構造化し、残りを開発者に任せる方法にいくつかの制限を課します。 私たちの場合、目的に基づいていくつかのGoパッケージに分割しました。

  • cfg:構成値をロード、解析、および提供します
  • cmd / loadcat:メインパッケージ、エントリポイントを含み、バイナリにコンパイルします
  • データ:「モデル」を含み、永続性のために埋め込まれたキー/値ストアを使用します
  • feline:構成ファイルの生成、リロードメカニズムなどのコア機能が含まれています。
  • ui:テンプレート、URLハンドラーなどが含まれます。

特に猫のパッケージ内のパッケージ構造を詳しく見ると、すべてのNGINX固有のコードがサブパッケージの猫/nginx内に保持されていることがわかります。 これは、残りのアプリケーションロジックを汎用的に保ち、将来的に他のロードバランサー(HAProxyなど)のサポートを拡張できるようにするために行われます。

エントリーポイント

「cmd/loadcatd」内にあるLoadcatのメインパッケージから始めましょう。 主な機能であるアプリケーションのエントリポイントは、3つのことを行います。

 func main() { fconfig := flag.String("config", "loadcat.conf", "") flag.Parse() cfg.LoadFile(*fconfig) feline.SetBase(filepath.Join(cfg.Current.Core.Dir, "out")) data.OpenDB(filepath.Join(cfg.Current.Core.Dir, "loadcat.db")) defer data.DB.Close() data.InitDB() http.Handle("/api", api.Router) http.Handle("/", ui.Router) go http.ListenAndServe(cfg.Current.Core.Address, nil) // Wait for an “interrupt“ signal (Ctrl+C in most terminals) }

物事をシンプルに保ち、コードを読みやすくするために、すべてのエラー処理コードが上記のスニペットから(およびこの記事の後半のスニペットからも)削除されました。

コードからわかるように、「-config」コマンドラインフラグ(現在のディレクトリではデフォルトで「loadcat.conf」)に基づいて構成ファイルをロードしています。 次に、いくつかのコンポーネント、つまりコアネコパッケージとデータベースを初期化します。 最後に、WebベースのGUI用のWebサーバーを起動しています。

構成

ここでは、構成ファイルのロードと解析がおそらく最も簡単な部分です。 TOMLを使用して構成情報をエンコードしています。 Goで利用できるきちんとしたTOML解析パッケージがあります。 ユーザーからの構成情報はほとんど必要ありません。ほとんどの場合、これらの値の適切なデフォルトを決定できます。 次の構造体は、構成ファイルの構造を表しています。

 struct { Core struct { Address string Dir string Driver string } Nginx struct { Mode string Systemd struct { Service string } } }

そして、典型的な「loadcat.conf」ファイルは次のようになります。

 [core] address=":26590" dir="/var/lib/loadcat" driver="nginx" [nginx] mode="systemd" [nginx.systemd] service="nginx.service"

ご覧のとおり、TOMLでエンコードされた構成ファイルの構造とその上に示されている構造には類似点があります。 構成パッケージは、構造体の特定のフィールドにいくつかの適切なデフォルトを設定することから始まり、次にその上で構成ファイルを解析します。 指定されたパスで構成ファイルが見つからない場合は、構成ファイルを作成し、最初にデフォルト値をダンプします。

 func LoadFile(name string) error { f, _ := os.Open(name) if os.IsNotExist(err) { f, _ = os.Create(name) toml.NewEncoder(f).Encode(Current) f.Close() return nil } toml.NewDecoder(f).Decode(&Current) return nil }

データと永続性

ボルトとの出会い。 純粋なGoで記述された組み込みのKey/Valueストア。 非常にシンプルなAPIを備えたパッケージとして提供され、すぐに使用できるトランザクションをサポートし、非常に高速です。

パッケージデータ内には、各タイプのエンティティを表す構造体があります。 たとえば、次のようになります。

 type Balancer struct { Id bson.ObjectId Label string Settings BalancerSettings } type Server struct { Id bson.ObjectId BalancerId bson.ObjectId Label string Settings ServerSettings }

…ここで、バランサーのインスタンスは単一のロードバランサーを表します。 Loadcatを使用すると、NGINXの単一インスタンスを介して複数のWebアプリケーションのリクエストのバランスを効果的にとることができます。 すべてのバランサーの背後に1つ以上のサーバーを配置でき、各サーバーを個別のバックエンドノードにすることができます。

Boltはキー値ストアであり、高度なデータベースクエリをサポートしていないため、これを行うアプリケーション側のロジックがあります。 Loadcatは、それぞれに数千台のサーバーを備えた数千台のバランサーを構成するためのものではないため、当然、この単純なアプローチは問題なく機能します。 また、Boltはバイトスライスであるキーと値を処理するため、構造体をBoltに格納する前にBSONでエンコードします。 データベースからBalancer構造体のリストを取得する関数の実装を以下に示します。

 func ListBalancers() ([]Balancer, error) { bals := []Balancer{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { bal := Balancer{} bson.Unmarshal(v, &bal) bals = append(bals, bal) } return nil }) return bals, nil }

ListBalancers関数は、読み取り専用トランザクションを開始し、「バランサー」バケット内のすべてのキーと値を反復処理し、各値をBalancer構造体のインスタンスにデコードして、配列で返します。

バランサーをバケットに保管するのもほぼ同じくらい簡単です。

 func (l *Balancer) Put() error { if !l.Id.Valid() { l.Id = bson.NewObjectId() } if l.Label == "" { l.Label = "Unlabelled" } if l.Settings.Protocol == "https" { // Parse certificate details } else { // Clear fields relevant to HTTPS only, such as SSL options and certificate details } return DB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) p, err := bson.Marshal(l) if err != nil { return err } return b.Put([]byte(l.Id.Hex()), p) }) }

Put関数は、特定のフィールドにいくつかのデフォルト値を割り当て、HTTPSセットアップで添付されたSSL証明書を解析し、トランザクションを開始し、構造体インスタンスをエンコードして、バランサーのIDに対してバケットに格納します。

SSL証明書の解析中に、標準のパッケージエンコーディング/ PEMを使用して2つの情報が抽出され、DNS名とフィンガープリントの2つの情報がSSLOptions[設定]フィールドに保存されます。

バランサーでサーバーを検索する機能もあります。

 func ListServersByBalancer(bal *Balancer) ([]Server, error) { srvs := []Server{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("servers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { srv := Server{} bson.Unmarshal(v, &srv) if srv.BalancerId.Hex() != bal.Id.Hex() { continue } srvs = append(srvs, srv) } return nil }) return srvs, nil }

この関数は、私たちのアプローチが実際にどれほど素朴であるかを示しています。 ここでは、「サーバー」バケット全体を効果的に読み取り、配列を返す前に無関係なエンティティを除外しています。 しかし、繰り返しになりますが、これは問題なく機能し、変更する本当の理由はありません。

サーバーのPut関数は、デフォルトと計算フィールドを設定するコードの行をそれほど必要としないため、 Balancer構造体の関数よりもはるかに単純です。

NGINXの制御

Loadcatを使用する前に、生成された構成ファイルをロードするようにNGINXを構成する必要があります。 Loadcatは、バランサーのID(短い16進文字列)によってディレクトリの下に各バランサーの「nginx.conf」ファイルを生成します。 これらのディレクトリは、 cwdの「out」ディレクトリの下に作成されます。 したがって、これらの生成された構成ファイルをロードするようにNGINXを構成することが重要です。 これは、「http」ブロック内の「include」ディレクティブを使用して実行できます。

/etc/nginx/nginx.confを編集し、「http」ブロックの最後に次の行を追加します。

 http { include /path/to/out/*/nginx.conf; }

これにより、NGINXは「/ path / to / out /」の下にあるすべてのディレクトリをスキャンし、各ディレクトリ内で「nginx.conf」という名前のファイルを探し、見つかった各ディレクトリをロードします。

コアパッケージであるfelineでは、インターフェイスDriverを定義しています。 正しいシグネチャを持つGenerateReloadの2つの関数を提供する構造体は、ドライバとしての資格があります。

 type Driver interface { Generate(string, *data.Balancer) error Reload() error }

たとえば、feline/ nginxパッケージの下のstructNginxは次のとおりです。

 type Nginx struct { sync.Mutex Systemd *dbus.Conn } func (n Nginx) Generate(dir string, bal *data.Balancer) error { // Acquire a lock on n.Mutex, and release before return f, _ := os.Create(filepath.Join(dir, "nginx.conf")) TplNginxConf.Execute(f, /* template parameters */) f.Close() if bal.Settings.Protocol == "https" { // Dump private key and certificate to the output directory (so that Nginx can find them) } return nil } func (n Nginx) Reload() error { // Acquire a lock on n.Mutex, and release before return switch cfg.Current.Nginx.Mode { case "systemd": if n.Systemd == nil { c, err := dbus.NewSystemdConnection() n.Systemd = c } ch := make(chan string) n.Systemd.ReloadUnit(cfg.Current.Nginx.Systemd.Service, "replace", ch) <-ch return nil default: return errors.New("unknown Nginx mode") } }

Generateは、出力ディレクトリへのパスとBalancer構造体インスタンスへのポインタを含む文字列を使用して呼び出すことができます。 Goは、テキストテンプレートの標準パッケージを提供します。これは、NGINXドライバーが最終的なNGINX構成ファイルを生成するために使用します。 テンプレートは、バランサーの構成方法に基づいて生成された「アップストリーム」ブロックとそれに続く「サーバー」ブロックで構成されます。

 var TplNginxConf = template.Must(template.New("").Parse(` upstream {{.Balancer.Id.Hex}} { {{if eq .Balancer.Settings.Algorithm "least-connections"}} least_conn; {{else if eq .Balancer.Settings.Algorithm "source-ip"}} ip_hash; {{end}} {{range $srv := .Balancer.Servers}} server {{$srv.Settings.Address}} weight={{$srv.Settings.Weight}} {{if eq $srv.Settings.Availability "available"}}{{else if eq $srv.Settings.Availability "backup"}}backup{{else if eq $srv.Settings.Availability "unavailable"}}down{{end}}; {{end}} } server { {{if eq .Balancer.Settings.Protocol "http"}} listen {{.Balancer.Settings.Port}}; {{else if eq .Balancer.Settings.Protocol "https"}} listen {{.Balancer.Settings.Port}} ssl; {{end}} server_name {{.Balancer.Settings.Hostname}}; {{if eq .Balancer.Settings.Protocol "https"}} ssl on; ssl_certificate {{.Dir}}/server.crt; ssl_certificate_key {{.Dir}}/server.key; {{end}} location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://{{.Balancer.Id.Hex}}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; } } `))

リロードは、NGINXに構成ファイルをリロードさせるNginx構造体のもう1つの機能です。 使用されるメカニズムは、Loadcatの構成方法に基づいています。 デフォルトでは、 [sudo] systemd reload nginx.serviceが機能するように、NGINXはnginx.serviceとして実行されるsystemdサービスであると想定しています。 ただし、シェルコマンドを実行する代わりに、パッケージgithub.com/coreos/go-systemd/dbusを使用してD-Busを介してsystemdへの接続を確立します。

WebベースのGUI

これらすべてのコンポーネントを配置したら、プレーンなBootstrapユーザーインターフェイスですべてをまとめます。

シンプルなGUIにラップされたNGINX負荷分散機能

シンプルなGUIにラップされたNGINX負荷分散機能
つぶやき

これらの基本的な機能については、いくつかの単純なGETおよびPOSTルートハンドラーで十分です。

 GET /balancers GET /balancers/new POST /balancers/new GET /balancers/{id} GET /balancers/{id}/edit POST /balancers/{id}/edit GET /balancers/{id}/servers/new POST /balancers/{id}/servers/new GET /servers/{id} GET /servers/{id}/edit POST /servers/{id}/edit

これらはほとんどCRUDページであるため、個々のルートを通過することは、ここで行うのが最も興味深いことではない場合があります。 パッケージのUIコードを自由に見て、これらの各ルートのハンドラーがどのように実装されているかを確認してください。

各ハンドラー関数は、次のいずれかのルーチンです。

  • データストアからデータをフェッチし、レンダリングされたテンプレートで応答します(フェッチされたデータを使用)
  • 受信フォームデータを解析し、データストアに必要な変更を加え、パッケージネコを使用してNGINX構成ファイルを再生成します

例えば:

 func ServeServerNewForm(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) TplServerNewForm.Execute(w, struct { Balancer *data.Balancer }{ Balancer: bal, }) } func HandleServerCreate(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) r.ParseForm() body := struct { Label string `schema:"label"` Settings struct { Address string `schema:"address"` } `schema:"settings"` }{} schema.NewDecoder().Decode(&body, r.PostForm) srv := data.Server{} srv.BalancerId = bal.Id srv.Label = body.Label srv.Settings.Address = body.Settings.Address srv.Put() feline.Commit(bal) http.Redirect(w, r, "/servers/"+srv.Id.Hex()+"/edit", http.StatusSeeOther) }

ServeServerNewForm関数が行うのは、データストアからバランサーをフェッチし、テンプレート(この場合はTplServerList )をレンダリングすることだけです。このテンプレートは、バランサーのServers関数を使用して関連するサーバーのリストを取得します。

一方、 HandleServerCreate関数は、本体からの着信POSTペイロードを構造体に解析し、それらのデータを使用してデータストア内の新しいサーバー構造体をインスタンス化および永続化してから、パッケージfelineを使用してバランサーのNGINX構成ファイルを再生成します。

すべてのページテンプレートは「ui/templates.go」ファイルに保存され、対応するテンプレートHTMLファイルは「ui/templates」ディレクトリにあります。

試してみる

Loadcatをリモートサーバーまたはローカル環境にデプロイするのは非常に簡単です。 Linux(64ビット)を実行している場合は、リポジトリの[リリース]セクションから、ビルド済みのLoadcatバイナリを含むアーカイブを取得できます。 少し冒険心がある場合は、リポジトリのクローンを作成して、自分でコードをコンパイルできます。 ただし、Goプログラムのコンパイルは実際には難しいことではないため、この場合の経験は少しがっかりするかもしれません。 また、Arch Linuxを実行している場合は、幸運です。 配布用のパッケージが便利なように作成されています。 ダウンロードして、パッケージマネージャーを使用してインストールするだけです。 関連する手順の概要は、プロジェクトのREADME.mdファイルに記載されています。

Loadcatを構成して実行したら、Webブラウザーで「http:// localhost:26590」を指定します(ローカルで実行され、ポート26590でリッスンしていると想定します)。 次に、バランサーを作成し、いくつかのサーバーを作成し、それらの定義されたポートで何かがリッスンしていることを確認します。これで、実行中のサーバー間でNGINXが着信要求の負荷を分散する必要があります。

次は何ですか?

このツールは完璧にはほど遠いものであり、実際にはかなり実験的なプロジェクトです。 このツールは、NGINXのすべての基本機能を網羅しているわけではありません。 たとえば、NGINXレイヤーのバックエンドノードによって提供されるアセットをキャッシュする場合でも、NGINX構成ファイルを手動で変更する必要があります。 そしてそれが物事をエキサイティングにするものです。 ここでできることはたくさんあり、それがまさに次のことです。NGINXの負荷分散機能のさらに多くをカバーします-基本的な機能、そしておそらくNGINXPlusが提供しなければならない機能です。

Loadcatを試してみてください。 コードをチェックして、フォークして、変更して、遊んでください。 また、他のソフトウェアを構成するツールを作成したか、または下のコメントセクションで本当に気に入ったツールを使用したかどうかをお知らせください。