はじめに
本記事ではDocker composeを基準に進めますが、CLIでも同様です。
コンテナ間通信
Dockerのコンテナ名とネットワーク
Dockerのコンテナ間通信について基本知識を知っておきましょう。
DockerコンテナはDocker networkを介してコンテナ間通信を行うことができます。
同じネットワークに接続されているコンテナ同士は、
http://[コンテナ名]:[ポート(オプション)]
というURIでアクセス可能です。
コンテナ名はコンテナに与えられる一意の名前で、デフォルトでは
[プロジェクト名]_[サービス名]_[連番]
の形で自動生成されます。
container_nameキーを使って明示的にコンテナ名を指定することも可能です。
いずれの場合でも、コンテナ名はDocker全体で一意でなければなりません。
例えば以下のcomposeは起動失敗します。
services:
container1:
container_name: app
...
container2:
container_name: app # appの重複
...
$ docker compose up -d
> services.container2: container name "app" is already in use by service {}"
ネットワークエイリアス
引き続きネットワークエイリアスについても知っておきましょう。
コンテナは、ネットワーク範囲のエイリアス(別名)を持つことが可能です。
例えば、下のcomposeを見てください。
services:
mysql:
container_name: mysql
networks:
db_network: # db_networkに参加し、dbというエイリアスを持つ
aliases:
- db
app:
container_name: app
networks:
- db_network: # db_networkに参加
networks:
db_network: # appとmysqlが共に参加するネットワーク
このとき、appコンテナはmysqlに2通りの名前でアクセスが可能です。
すなわち
http://mysql
http://db
エイリアスは複数指定することが可能です。
重要な点として、エイリアスはコンテナ間通信においてのみコンテナ名の代わりとして使用できます。
そのため、以下のように使うことはできません。
$ docker compose up db -d
> Error response from daemon: No such container: db
エイリアスの競合
エイリアスについて理解したところで、本題です。
このエイリアス、一意であることを強制されません。
複数のコンテナに同じエイリアスを割り当てることができます。
実行してもエラーになりません。
実験をしてみましょう。
エイリアス競合実験
概要
自分に割り当てられた名前を返すシンプルなコンテナを複数用意し、同じエイリアスを指定して様子を見てみましょう。
今回はapp_a、app_b、app_cの3つのコンテナを仮定します。
まとめて「応答コンテナ」と呼びましょう。
応答コンテナは実験のために単純化します。
curlでアクセスされたとき、自分に割り当てられた名前を返すだけのものです。
3つの応答コンテナには、全て同じエイリアスappを割り当てます。
curlを送出するのは観測用のメインコンテナmainです。
応答コンテナとメインコンテナは共通のネットワークtest_netに接続します。
各応答コンテナに、メインコンテナからアクセスする、というシナリオです。
実験準備
./ #
├── Dockerfile # 応答コンテナ用
├── compose.yml
├── entrypoint.sh # 応答コンテナビルド用
└── main.Dockerfile # 応答コンテナに接続するメインコンテナ
FROM nginx:alpine
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
CMD ["/entrypoint.sh"]
services:
app_a: # 応答コンテナ1つ目
build: .
container_name: app_a
environment:
NAME: app A
networks:
test_net:
aliases:
- app
app_b: # 応答コンテナ2つ目
build: .
container_name: app_b
environment:
NAME: app B
networks:
test_net:
aliases:
- app
app_c: # 応答コンテナ3つ目
build: .
container_name: app_c
environment:
NAME: app C
networks:
test_net:
aliases:
- app
app_main: # メインコンテナ
container_name: main
build:
context: .
dockerfile: main.Dockerfile
tty: true
networks:
- test_net
networks:
test_net:
echo "Hi I'm ${NAME} container!" > /usr/share/nginx/html/index.html
nginx -g "daemon off;"
FROM alpine
RUN apk update && apk add curl
ファイルが揃ったディレクトリで以下のコマンドを実行して、実験を始めましょう。
docker compose up -d
docker exec -it main sh # 観測のためにメインコンテナに入る
実験① コンテナ名でアクセスしてみる
curl http://app_a
curl http://app_b
curl http://app_c
Hi I'm app A container!
Hi I'm app B container!
Hi I'm app C container!
ちゃんと対応するコンテナが応答していますね。
では、エイリアスでやってみましょう。
実験② エイリアスでアクセスしてみる
curl http://app
何回かこのコマンドを叩いてみてください。
Hi I'm app B container!
Hi I'm app A container!
Hi I'm app B container!
Hi I'm app C container!
Hi I'm app A container!
...
無作為に応答コンテナへ繋いでいます。
解説
DNSラウンドロビン
ここまででピンときた方もいらっしゃるかもしれません。
エイリアスを指定した際の無作為な応答は、DNSラウンドロビンの挙動そのものです。
DNSラウンドロビンの概要
DNSラウンドロビンのWikipediaには
一つのドメイン名に複数のIPアドレスを割り当てる負荷分散技術
とあります。
通常DNSのAレコードは、1エントリにつきドメインとアドレスが一対一で対応します。
hoge IN A 127.0.1.1
hogeへのリクエストは、127.0.1.1が応答します。
では以下のレコードはどうでしょう。
hoge IN A 127.0.1.1
hoge IN A 127.0.1.2
hoge IN A 127.0.1.3
このレコードは、hogeへのリクエストに対し、127.0.0.1、127.0.0.2、127.0.0.3の3つが応答可能です。
同じドメインで異なるアドレスのAレコードを複数登録すると、そのドメインに紐づけられたアドレスを順繰りで返すことになっています。
この挙動は、1つのドメインに来るアクセスを複数のアドレス先に分散させてサーバーあたりの負荷を軽減するという、原始的なロードバランサとして機能することになっています。
これがDNSラウンドロビンです。
ラウンドロビンの偏り
前項で、”なっている”と強調したのは、実際に登録された中から返されるアドレスはきれいな順繰り(ローテーション)でも、均等なランダムでもないからです。
詳しい説明は省きますが、ラウンドロビンで返されるアドレスは様々な要因によって大きく偏りが生じます。
実験②のコマンドを叩き続けると、実際にある程度の偏りを観察できるでしょう。
この偏り以外にも、ロードバランサーとしてのDNSラウンドロビンは、使い勝手が悪い問題点を数多く抱えており、現在では積極的に採用されることは少なくなっています。
DockerのDNS
Dockerはデーモンの中にDNSサーバーを内蔵しており、コンテナの名前解決を実現しています。
ネットワークエイリアスは、コンテナと紐づけられたレコードとして内蔵DNSに正常に登録されます。
重複したエイリアスは同じドメインに対する異なるアドレスとして登録され、結果DNSラウンドロビン状態になるというわけです。
結論と感想
意図してDocker内ラウンドロビンを作る必要性はほぼない気がします。
余程尖った需要がない限り、コンテナ名を使うほうが確実で安心ですね。