Evolutionary Architectures

takezawa's blog

Goで競技プログラミングをやろう

この記事は Kyash Advent Calendar 2020 7日目の記事です。

最近、競技プログラミング(以下、競プロと略します。)にGo言語で参加していまして、私自身はGoでやる事に特には問題を感じておりませんが、競プロでの経験が少ない方だと言語差など気にする方もいると思いますので、そのへんを説明してみるという記事になります。

結論としては、少なくともAtCoderでGoを使うのはそれほど不利ではないので、Goでやってみたいという方は細かい事を気にせずにチャレンジしてみてください。(どの言語でも良いっていうなら、やはりC++のほうが不便が少ないとは思います。) 私は競プロをするようになって、Goに関してはslice周辺への理解が一層深まりました。

競プロの話をする上での前提

まず前提です。ここでいう競プロは特に日本人の参加者も多く、敷居が低いと思われるAtCoderのコンテストを対象としたものになります(他にはCodeforces情報オリンピックなど様々なプログラミングコンテストがあります)。また言語差の視点についてですが、こういった競プロではC++が圧倒的に使われていますので、基本的にはC++と比べてどうなのか、という話をします。

競プロを全く知らない、という方はまず試しに例えば以下の問題を解いて、AtCoderのオンラインジャッジシステムに触れてみると良いでしょう。ユーザ登録してページ末尾のフォームからソースコードをSubmitすることで回答することができます。(ちなみにこの問題は最も簡単な部類であり、問題文が理解できれば題意を率直に実装するだけで良いので、競プロ的な難しさや楽しさなどは全くありません。あくまでオンラインジャッジシステムを知るための例としてとらえてください。) atcoder.jp

Goは遅くて不利?

まず不安視される点として、Goは遅くて不利なんじゃないか、というような点があるかと思います。しかし、競技プログラミングで不利になるような遅さではありません。遅さのせいでC++より解き辛くなるような問題を見つけるのは困難でしょう。

Goには、ランタイムやGCなどのオーバーヘッドがあり、オーバーヘッドを除いた実行速度でもC++の倍ぐらい遅いこともあるかもしれません。しかし、仮に数倍程度遅かったとしても競プロにおいてはほとんど問題ありません。

このあたりをある程度納得するには計算量についての理解が必要となってきます。また、AtCoderでは1秒あたり 10^{8}回くらいのステップを実行でき、問題の入力で与えられるデータ数は多くておおよそ  10^{5} 個、実行時間制限は例えば2秒以内となっています。このデータ数を  n としたとき、仮に時間計算量が O(n^{2})になるような実装をしてしまうと制限時間には絶対に間に合いません。 n^{2} が最大で [tex: 1010] なので、100秒かかってしまうと見積もれるからですね。計算量を改善しない限り、いくらその他の細かな最適化を行っても到底そこから100倍速とはならず、間に合いません*1。よってより効率的な解法を考え、例えば  O(n) O(n \log^{2} n) などの計算量になる実装をしなくてはいけません。これはどのプログラミング言語を使ってもほぼ同様です。なぜなら言語間の差はオーダー記法でいうと定数倍の差しか作らないからです*2

例えば以下の問題は、そういった計算量の知識なしに解いてしまうと、なぜ時間制限をオーバーしてしまうのかわからなくなってしまうと思います。なお、この問題に関しては入力のバッファリングも必要になります。バッファリングについては本記事内でやり方を説明します。

atcoder.jp

以下のようなループをするコードを書いてしまうと、[tex: 1010]ステップを軽く超え、やはり制限時間には到底間に合いません。

var n, w int
fmt.Scan(&n, &w)
for i := 0; i < n; i++ {
    var s, t, p int
    fmt.Scan(&s, &t, &p)
    for ; s < t; s++ {
        // ここがトータルで 10^10 回以上実行されることになる。
    }
}

以上のように、制限時間に間に合うような解法を「考察」して見つける、というのが競プロでいつもやらなければいけない事でして、簡単すぎる問題でなければ、競技中はここに最も時間を割きます。この「考察」においては言語間の差は著しく低いでしょう。

C++は標準ライブラリが便利そうだが、Goの標準ライブラリは不利?

ライブラリについては、C++のほうがわずかに有利といえるでしょう。

標準ライブラリの差という点でGoで最も不利を感じるのは、Ordered Mapの欠如でしょう。例えばC++であれば std::map::lower_bound を使って、 std::map 内の特定のキーから近い別のキーを取得することができますが、Goの map は順序付けされていないのでそういったことができません。しかし AtCoder の実行環境においては、 https://github.com/emirpasic/gods という外部ライブラリを使うことができます*3 ので、そのRedBlackTreeを利用すれば、C++std::mapと同様の操作が可能です。(とはいえ、やはり組み込みで用意されている slice や map などの使い心地には及びません。)

また、 AtCoderが最近公開したAtCoder LibraryというC++アルゴリズムライブラリがありますが、もちろんC++以外では利用できません*4。 ただ、このようなアルゴリズムライブラリはそもそも学習しながら自分で準備しておくべきもの、といえると思いますし、もっと様々なコードをスニペットとして準備しておけばいざというときに有利になります。

Goの良いところはある?

些細な事ではありますが、しばしばGoでよかったと思える部分を挙げておきます。

  • AtCoderのジャッジシステムは64bit環境でありint型が64bitサイズになっているため、int64型をわざわざ使い分けなくて良いのは楽です。
  • sliceの先頭と末尾からであれば要素を  O(1) で削除できます*5C++では std::vector で先頭削除をやろうとすると遅いので、代わりに std::liststd::deque を使ったり、先頭を指すインデックスを用意しておいてそれをずらすことで表現するでしょう。
  • 関数が多値を返すことができます。C++でも std::tuple を使えば同様のことができますが、Goのように言語組み込みの機能であれば、より使いやすいです。
  • Golandからであれば、テストを生成し、デバッガを使ったデバッグがやりやすいです。

Goで挑戦するなら、まずは入出力をバッファリングするやり方を覚えよう

先ほど、入力が多い場合は  10^{5} 個になる、ということを書きましたが、こういった大量の入出力をするためにはバッファリングをしないと実行時間制限に間に合わなくなります。なおバッファリングをしないと間に合わない、というのはGoに限ったことではなく他の言語でも同じ事です。

Goでバッファリングをするには基本的には bufio.NewReaderbufio.NewWriter を使えばよいでしょう。 これを使って例えば先程の atcoder.jp を解くと以下のようになります。

func main() {
    in := bufio.NewReader(os.Stdin)
    out := bufio.NewWriter(os.Stdout)
    defer out.Flush() // バッファリングしているので、最後に確実に Flush して出力させます。

    var n, w int
    fmt.Fscan(in, &n, &w) // 入力

    sum := make([]int, 2*1e5+2)
    for i := 0; i < n; i++ {
        var s, t, p int
        fmt.Fscan(in, &s, &t, &p) // 入力
        sum[s] += p
        sum[t] -= p
    }
    for i := 0; i < 2*1e5+1; i++ {
        sum[i+1] += sum[i]
    }
    for i := 0; i < 2*1e5+2; i++ {
        if sum[i] > w {
            fmt.Fprintln(out, "No") // 出力
            return
        }
    }
    fmt.Fprintln(out, "Yes") // 出力
}

バッファサイズをもっとチューニングしたり、 bufio.Scanner を使ったりなどすれば、さらに高速化することはできますが、そこまでせずとも bufio.NewReaderbufio.NewWriter で速度的には十分です。

よく使うGoの標準ライブラリ

参考までに、私が特に競プロで使う標準ライブラリを挙げておきます。

  • sort パッケージ
    • sort.Ints
    • sort.Float64s
    • sort.Strings
    • sort.Sort
    • sort.Slice
    • sort.Search
    • sort.SearchInts
  • container/heap パッケージ
    • 優先度付きキューの実装に使います。
  • container/list パッケージ
    • デック(Deque)の実装に使います。
  • math パッケージ
    • math.Hypot
  • math/bits パッケージ
    • bits.OnesCount

AtCoder 以外のコンテストはどうなのか?

*1:例えば、ありがちな些細な最適化としては、関数をインライン化する、変数を減らす、のようなやつでしょう。

*2:一部のスクリプト言語などかなり遅い処理系の場合は、この定数倍が大きすぎて問題になることもあるかもしれません。

*3:詳しくは、Language Test 202001 - AtCoderにある表のうちGoの項目を参照してください。他にはhttps://github.com/gonum/gonumを利用できます。

*4:有志の方で移植されている https://github.com/monkukui/ac-library-go はありますが、まだ不足しているといえます。発展させたいですね。

*5:sliceはいろんな操作ができます。参考: https://github.com/golang/go/wiki/SliceTricks

QUICの紹介

この記事は Kyash Advent Calendar 2019 21日目の記事です。

現在IETFによって HTTP/3 という HTTP/2 に続くHTTPの新しいバージョンの仕様策定が進められています。HTTP/3 は QUIC というプロトコル上で動作するものです。今回は特に QUIC について紹介します。

QUICとは

QUIC は現在IETFによって標準化が進められているプロトコルです。簡単に言うと UDP 上に再実装された TCP+TLS のようなものです。冗談で TCP/2 と呼ぶ人もいる*1ようです。QUIC は TLS 1.3 の仕組みを使った暗号化が必須となっており、暗号化せずに QUIC を使うことはできません。

なぜ QUIC という新しいプロトコルが開発されているのか

QUIC は Google によって発案*2されました。そもそも Google からはウェブ高速化のために TCP を改善するための提案*3が幾度もされてきています。しかし TCP は世界中のネットワーク機器のファームウェアや、カーネルに実装されているため、必要なアップグレードの量を考えると TCP に大きな変更を加えてすぐに恩恵を受けることはほとんど不可能といえます。

その点、QUIC の場合はユーザランドUDP 上に実装されるため、ファームウェアカーネルの制約を受けずに変更を行うことができます。またほとんどのデータが暗号化されていることにより、ルーターなど途中のネットワーク機器による介入が難しく、プロトコルの進化が阻害されにくいようになっています。

また QUIC の用途は HTTP だけが想定されているわけではなく、DNS over QUIC や、RTP over QUIC といったプロトコルも提案されています。

QUIC の特徴

QUIC には以下のような目標*4があります。

  1. インターネットでの広範な展開可能性(カーネルの変更や権限昇格をせずに一般的なユーザクライアントでされる)。
  2. パケット損失による Head-of-line blocking の低減 (1つのパケット損失により他の多重化ストリームを損なわない)。
  3. 低遅延 (セットアップや再開時およびパケット損失の応答時のラウンドトリップコストの最小化)。
  4. 遅延や効率においてモバイルのサポートの改善 (無線の切断により壊れてしまうTCP接続とは対照的です)。
  5. TCP に匹敵し使いやすい輻輳回避のサポート。
  6. TLS に匹敵するプライバシー保証 (順番通りに転送することや複合することを必要としない)。
  7. サーバサイドとクライアントサイドで信頼性が高く安全なリソース要件のスケーリング (合理的なバッファー管理とアンプ攻撃の回避支援も含む)。
  8. 帯域の消費を減らし、チャネルステータスの応答性を向上 (すべての多重化ストリームにわたるチャネルステータスの統一的なシグナリングによる)。
  9. 他の目標と矛盾していない場合、パケット数を削減。
  10. 多重化ストリームのための信頼できるトランスポートをサポート (多重化ストリーム上で TCP をシミュレートできます)。
  11. 他の目標と矛盾していない場合、プロキシのための効率的なdemux-mux(多重分離化-多重化)特性。
  12. 述べた目標を犠牲にせず、可能な限り任意の時点で既存のプロトコルを再利用または進化させます (例えばuTP(Ledbat)、DCCP、TCP など)

  13. の展開可能性については前節で述べました。他の項目についてもいくつか抜粋して説明をします。

Head-of-line blocking の削減について

TCP は順番にパケットを受け取って処理するプロトコルであるため、先行するパケットが欠損した場合、後続のパケットの受け取りがブロックされる Head-of-line blocking という問題があります。例えば HTTP/2 のように TCP 上でストリームが多重化されている場合、あるストリームでパケット欠損が起こると、他のストリームをブロックしてしまいます。

QUIC は TCP を使わず、受信する順序に依存しない UDP を用い、パケット単位で暗号を復号できるようにすることでこの問題を削減します。QUIC のストリームは独立しているため、あるストリームのパケット損失が他のストリームに影響することはほとんどありません。

ラウンドトリップコストの最小化について

TCP 上で TLS を使って接続する場合は、TCP のハンドシェイク分も含めて、3-RTT を必要とします。

QUIC は、UDP を用いて TLS1.3 も統合的に実装されたことにより、新規接続の場合で 1-RTT、再接続の場合は 0-RTT で接続することができます。

モバイルのサポートの改善 (ローミング)

モバイルでよくあるようにキャリア通信とWi-Fi通信が切り替わったときなどは、通信に使うIPとポート番号が変わります。TCP ではこのような場合、コネクションを作り直さなければなりません。

QUIC では、コネクションごとにコネクションIDというものを割り当ててコネクションを識別するので、IPとポート番号が変わった場合でもコネクションを継続することができ、ネットワークエラーになる場合を減らすことができます。

既存の実装

まだ仕様はドラフトの段階ですが、すでに実装はいくつもあり相互接続テスト*5が行われています。 試すのであれば HTTP/3: the past, the present, and the future を参考にして、Quiche や Google Chrome を使うのがお手軽です。keylogファイルを出力してドラフトの対応バージョンに気をつければ、Wireshark でパケットの内容を詳細に観察することもできます。

f:id:ttakezawa:20191221231501p:plain

とても勉強になるプロトコル

QUIC はユーザランドで実装されるプロトコルなので、TCP と比べてかなりいじりやすいプロトコルです。さらに QUIC は TCPTLSアルゴリズムを煮詰めて再実装しているプロトコルでもあるので、かなり学びがいがあります。

というわけで調べていると、実装してみたいモチベーションが出てきたので、自分で QUIC の実装をしてみています。まだまだ理解が追いついていない部分も多いのですが、これからがんばっていきます。

参考

データベーススキーマの差分を解消するツールを作った話

この記事は Kyash Advent Calendar 2019 2日目の記事です。

今回はKyashで利用しているPostgreSQLにおいて、スキーマの差分を解消するツールを作った話についてです。

ツールのリポジトリはこちらになります。 github.com

このツールは、2つのPostgreSQLインスタンスからpg_dumpすることにより得られたDDL間の差分を検出し、差分を解消するためのDDLを生成することができます。

社内システムで活用できることを最初のゴールとして開発しました。

こんなことができます。

例えば、入力用のDDLsource.sqldesired.sqlとして用意し、 source.sqlの状態からdesired.sqlの状態に変更するためのDDLを生成してみます。

pg_dumpを使うことを想定して最低限の実装をしているので、入力用のDDLがこんな感じになっています。

-- source.sql: 変更前のスキーマを表すDDL 
CREATE TABLE public.sessions (
  id bigint,
  name character(4)
);
-- desired.sql: 変更後のスキーマを表すDDL
CREATE TABLE public.sessions (
    id bigint NOT NULL,
    key character varying(255)
);

ALTER TABLE ONLY public.sessions
    ADD CONSTRAINT sessions_pkey PRIMARY KEY (id);

CREATE TABLE public.users (
    id bigint
);

CREATE SEQUENCE public.users_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;

ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);

これらを入力とし、変更するためのDDL(patch.sql)を生成します。

go run cmd/pgconverger/main.go -source "source.sql" -desired "desired.sql" > patch.sql

以下のようなDDL(patch.sql)が生成されます。

-- Table: "public"."sessions"
ALTER TABLE "public"."sessions" ALTER COLUMN "id" SET NOT NULL;
ALTER TABLE "public"."sessions" DROP COLUMN "name";
ALTER TABLE "public"."sessions" ADD COLUMN "key" character varying(255);
ALTER TABLE ONLY "public"."sessions" ADD CONSTRAINT "sessions_pkey" PRIMARY KEY ("id");

-- Table: "public"."users"
CREATE TABLE "public"."users" (
    "id" bigint
);
CREATE SEQUENCE "public"."users_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;
ALTER SEQUENCE "public"."users_id_seq" OWNED BY "users"."id";
ALTER TABLE ONLY "public"."users" ALTER COLUMN "id" SET DEFAULT "nextval"('public.users_id_seq'::"regclass");

経緯

Kyashの開発においては今のところスキーマ変更の適用が自動化されておらず(これから対応する予定のため)、本番環境、検証環境、開発環境、などの間で微妙に差分があり、差分が日々大きくなる、というようなことが起こっています。

こういった差分が起こってしまう主な要因としては、緊急で本番環境にインデックスを追加した場合や、開発や検証の目的で検証環境を変更したあとに、ちゃんと本番に適用してなかったり、その開発項目が中断や延期されたりした場合などに多かったと思います。
そのため、データベースに関する調査改善を行うときは、そもそもスキーマの差分がどうなっているか、などを確認するところからやる必要がありましたし、積もり積もってその差分はかなり大きくなっていました。
自動化しておけばいいだけの話なのですが、すぐに困るというほどのことでもなく、差分解消してからでないと何かしら問題が残ってしまうのでなかなか改善がされなかったわけです。

個人的にDDLの自動生成らへんは興味があるトピックだったので、自分の裁量で進められるよう他の開発者にもあまり影響のないような実装方針で、業務外の時間を使ってサイドプロジェクトとして開発をすることにしました。

完遂するために心がけたこと

「Kyashのスキーマ差分解消において役立てること」を最低限のゴールとして強く意識していました。

寄り道したり欲張ったりすると機能は途方もなく増えてしまいますし、どこまで考慮すればいいのかわからなくて設計もおぼつかなくなってしまいます。例えばCREATE TABLE文にはとてつもない表現力があり、サポート対象を絞らざるを得ません(参考: CREATE TABLE - PostgreSQL 11.5文書)。

個人開発をしていると様々なことを考えまくって結局終わらない、というのがよくあったのですが、今回のプロジェクトが目的を果たせたことで明確なゴールを持つ重要性が実感できました。

既存のツールを使っていない理由

自分で作ってみたかったという理由も大きいのですが、他の理由としては以下のような機能における差分を考慮したDDLを作成できるツールがなくて、Kyashのスキーマ差分解消というゴールを果たしづらかったためです。

実装の参考にしたもの

まず「Go言語でつくるインタプリタ」が大部分で参考になりました。この本を参考にしたおかげでPrattパーサという構文解析を学べましたし、すんなりとテストコードを書きながら実装ができました。

Go言語でつくるインタプリタ

Go言語でつくるインタプリタ

  • 作者:Thorsten Ball
  • 発売日: 2018/06/16
  • メディア: 単行本(ソフトカバー)

他には以下の資料やソースコードも実装方法の参考にしています。 特にLexical Scanning in Go - Rob PikeにおいてTokenizerとLexerを別々のgoroutineとして動かすアイデアや、Lexerがstate functionを返すことでstate machineを表現するアイデアは面白く、そのまま活用させて頂きました。

あとはひたすらPostgreSQLのドキュメントを参照しました。特に以下のページを使いまくりました。

改善できそうなところ

とにかくゴールを果たすために、今回はpg_dumpで出力したDDLをソースとして入力とすることを想定していたのですが、PostgreSQLのバージョンを変えるとpg_dumpの出力も変わることがありうるため変化に弱そうです。pg_dumpの出力は互換性も考慮されているためにシンプルとは言い難いDDLとなっています。

information_schemaを入力ソースとして扱う方法も考えていたのですが、こちらのほうが良かったかもしれません。データベースに接続しなければいけませんが構文解析の必要もなくなりそうです。

あとがき

定例の1on1でCTOの椎野に相談をして、サイドプロジェクトについてコーチングをしてもらっていました。コーチングの中でゴール設定も行え、無事に作り上げることができました。感謝してもしきれません。

NGINX Microservices Reference Architectureの3モデルを紹介

Nginx, Inc.のMicroservices Reference Architecture(MRA)についてのドキュメントでProxyモデル、Router Meshモデル、Fabricモデルという3つのネットワーキングモデルが解説されている。 GoFデザインパターン然り、名前が付いている、というのは重要なことだ。本項ではこの3モデルについて紹介する。

1. Proxyモデル

Proxyモデルはマイクロサービスアプリケーションのフロント側にリバースプロキシクラスターを配置する。

Proxy Model
出典元: MRA Part 2 – Proxy Model

Proxyモデルは比較的単純であり、API Gateway、初期のマイクロサービス、もしくは、複雑なレガシーモノリシックアプリケーションを変換する際のターゲットとして適している。特に大規模なマイクロサービスやトラフィックについての負荷分散に適しているようなモデルではないので、負荷分散への要件が厳しい場合にはRouter MeshモデルかFabricモデルを使わなければいけない。

このモデルは以下のような機能をもたらしうる。

パフォーマンスの最適化
  • Caching
  • Load Balancing
    • 動的なサービスディスカバリも併せて機能させれば、新しいサービスインスタンスをすぐに追加できる。
  • Low‑Latency Connectivity
    • ウェブブラウザやクライアントアプリなど外部からの接続に対して、HTTP/2サポートやHTTP/HTTPSのkeepaliveを機能させる。
  • High Availability
    • リバースプロキシの冗長化構成。
セキュリティと管理
マイクロサービス特有の機能
  • Central Communications Point for Services
    • マイクロサービスアプリケーションを使うクライアントは、通信において必要となるのは1箇所だけ。
  • Dynamic Service Discovery
  • API Gateway Capability
    • クライアントから受け取ったリクエストを必要に応じて変換しながら、サービスにルーティングする。
    • 複数リクエストの結果を集約して、単一レスポンスとしてクライアントに返す。

2. Router Meshモデル

Router Meshモデルは、Proxyモデルにおけるフロントのリバースプロキシクラスターに加え、ルーターメッシュハブのクラスターを配置する。ルーターメッシュハブはサービス間通信をハンドリングする。

Router Mesh Model
出典元: MRA Part 3 – Router Mesh Model

このモデルはより堅牢なアプリケーション設計として適しており、Fabricモデルほど複雑ではない。

Proxyモデルにおけるリバースプロキシの役割を2つのサーバクラスターに分けることとなる。2つのクラスターはそれぞれ以下のような機能を担うことができる。

リバースプロキシクラスタ
  • Caching
  • Low‑Latency Connectivity
  • High Availability
  • Rate Limiting / WAF
  • SSL/TLS Termination
  • HTTP/2 Support
ルーターメッシュハブクラスタ
  • Central Communications Point for Services
  • Dynamic Service Discovery
  • Load Balancing
  • Interservice caching
  • Health checks and the circuit breaker pattern

3. Fabricモデル

Fabricモデルでは各マイクロサービスインスタンスをホストするコンテナごとにプロキシサーバを配置する。このプロキシサーバはコンテナに出入りするすべてのHTTPトラフィックを扱うforward and reverseのプロキシサーバとなる。

Fabric Model
出典元: MRA Part 4 – Fabric Model

Router Meshモデルと違い、各マイクロサービスコンテナごとにプロキシを配置する。サービス間の通信においてアプリケーションはローカルホストのプロキシサーバとやり取りし、サービスディスカバリ、負荷分散、ヘルスチェックをプロキシサーバに委ねる。プロキシサーバがすべての接続の両端にあるので、その機能は特定のサーバやマイクロサービス特有の機能ではなく、アプリケーションを実行しているネットワークの性質となる。

このモデルは以下の問題を解決する。

  • 安全で高速な通信
    • マイクロサービス間のすべてのリクエストにSSL/TLSを使用して通信を安全にし、接続を永続化することでSSL/TLSのハンドシェイクといった処理を高速化する。
  • Service discovery
  • Load balancing
  • Resilience
    • すべてのマイクロサービスにおいてヘルスチェックを実行できるので、ネットワーク固有の性質としてサーキットブレーカーパターンを実装できる。

参考資料

あとがき

Proxyモデル、Router Meshモデル、Fabricモデルは、順番に複雑さを増していくモデルであり、この順に適応していくこともできます。もちろんマイクロサービスの実践においてはこれらに該当しない事も多いのですが、名前付けられたモデルがあれば議論がしやすくなると思い紹介に至りました。

今回は参考資料を部分的に抜粋しながら翻訳しました。参考資料では他にもNGINX Plusでの設定方法やサーキットブレーカー、ウェブフロントエンドの作り方など、MRAについて様々なことが詳しく書かれています。気になった事があれば是非参考元の資料を読んでみてください。 また、参考元ではNGINX Plusを採用する前提として説明されている部分が多いのですが、本項ではNGINX Plusについての特別な記載はすべて省きました。