Docker のマルチステージイメージ構築を理解する

Docker のマルチステージイメージ構築を理解する

Docker テクノロジーは、2013 年の誕生以来 4 年以上にわたって存在しています。日常の開発作業で Docker テクノロジーを受け入れて使用してきた開発者にとって、Docker イメージの構築は当たり前のことになっています。しかし、これは Docker のイメージ構築メカニズムが比較的完璧であることを意味するのでしょうか?いいえ、Docker の担当者はイメージ構築メカニズムを継続的に最適化しています。今年リリースされた Docker 17.05 バージョン以降、Docker はコンテナ イメージのマルチステージ ビルドのサポートを開始しました。

多段階イメージ構築とは何ですか?概念を直接定義するのはあまりにも唐突なので、ここでは秘密にしておきましょう。まず、日常の開発で使用されるイメージ構築方法と、イメージ構築で遭遇する問題から始めましょう。

1. 同型画像の構築

イメージを構築する際の一般的なシナリオは、アプリケーションが開発者自身の開発マシンまたはサーバー上で直接コンパイルされ、コンパイルされたバイナリ プログラムがイメージにインストールされるというものです。このような状況では通常、コンパイル環境がイメージで使用されるベース イメージと互換性があることが必要です。たとえば、Ubuntu 14.04 でアプリケーションをコンパイルし、Ubuntu シリーズのベースイメージに基づいたイメージにアプリケーションを配置します。アプリケーションのコンパイル環境は、それがデプロイされ実行される環境と互換性があるため、私はこの種のビルドを「同型イメージ ビルド」と呼んでいます。つまり、Ubuntu 14.04 でコンパイルしたアプリケーションは、ubuntu:14.04 以降のバージョン (例: 16.04、16.10、17.10 など) に基づくベース イメージでほぼシームレスに実行できます。ただし、Centos などの互換性のないベース イメージでは実行に失敗する可能性があります。

1. 均質画像構築の例

ここでは、均質なイメージを構築する例を示します (以降の章もこの例に基づいています)。注: コンパイル環境は、Ubuntu 16.04 x86_64 仮想マシン、Go 1.8.3、Docker 17.09.0-ce です。

Go で最も一般的な http サーバーを例として使用してみましょう。

  1. // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go
  2. パッケージメイン
  3.  
  4. 輸入 (
  5. 「ネット/http」  
  6. "ログ"  
  7. 「fmt」  
  8.  
  9. func home(w http.ResponseWriter, req *http.Request) {
  10. w.Write([]byte( "このウェブサイトへようこそ!\n" ))
  11. }
  12.  
  13. 関数main() {
  14. http.HandleFunc( "/" , ホーム)
  15. fmt.Println( "Webサーバーの起動" )
  16. fmt.Println( " -> ポート:1111 でリッスン" )
  17. エラー:= http.ListenAndServe( ":1111" 、nil)
  18. err != nil の場合 {
  19. log.Fatal( "ListenAndServe:" , err)
  20. }
  21. }

このプログラムをコンパイルします:

  1. # ビルド -o myhttpserver httpserver.go
  2. # ./myhttpserver
  3. ウェブサーバーの起動
  4. ->ポート:1111リッスン

この例は、数行のコードしかないため単純に見えますが、Go の net/http パッケージは、多くのシステム コールを含む多くの作業を最下層で実行します。これは、アプリケーションとオペレーティング システム間の「結合」を反映するもので、以降の説明で反映されます。次に、このプログラム用の Docker イメージを構築し、このイメージに基づいて myhttpserver コンテナを起動します。ベースイメージとしてubuntu:14.04を選択します。

  1. // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile
  2. ubuntu:14.04から
  3.  
  4. コピー ./myhttpserver /root/myhttpserver
  5. chmod +x /root/myhttpserver を実行します。
  6.  
  7. ワークディレクトリ /root
  8. エントリポイント [ "/root/myhttpserver" ]

ビルドを実行します。

  1. ビルドが成功すると、リポジトリは更新されます。
  2. ビルド コンテキストを Docker デーモン送信5.894MB
  3. ステップ 1/5: ubuntu:14.04から
  4. ---> dea1945146b9  
  5. ステップ 2/5: ./myhttpserver /root/myhttpserver をコピーします。
  6. ---> 993e5129c081  
  7. ステップ 3/5: chmod +x /root/myhttpserver を実行します。
  8. ---> 104d84838ab2 で実行中 
  9. ---> ebaeca006490  
  10. 中間コンテナ 104d84838ab2 を削除
  11. ステップ 4/5: WORKDIR /root
  12. ---> 7afdc2356149  
  13. 中間コンテナの取り外し 450ccfb09ffd
  14. ステップ 5/5: ENTRYPOINT /root/myhttpserver
  15. ---> 3182766e2a68 で実行中 
  16. ---> 77f315e15f14  
  17. 中間コンテナ 3182766e2a68 を削除しています
  18. 77f315e15f14 を正常に構築しました
  19. myrepo/myhttpserver:latest のタグ付けに成功しました
  20.  
  21. # Docker イメージ
  22. リポジトリ タグ イメージ ID 作成サイズ 
  23. myrepo/myhttpserver 最新 77f315e15f14 18 秒前 200MB
  24.  
  25. # docker 実行 myrepo/myhttpserver
  26. ウェブサーバーの起動
  27. ->ポート:1111リッスン

上記は最も基本的なイメージ構築方法です。

次に、次の要件に遭遇する可能性があります。

* Go プログラムのビルド環境の設定は、特に多くのサードパーティのオープンソース パッケージに依存する Go アプリケーションの場合、時間がかかることがあります。パッケージのダウンロードには長い時間がかかる場合があります。 Go プログラム構築のために、これらすべての変更可能なものをビルダー イメージにパッケージ化するのが最善です。

* 上記で作成した myrepo/myhttpserver イメージのサイズは 200 MB であり、少し「大きすぎる」ようです。各ホストノード上の Docker にはイメージレイヤーをキャッシュする機能がありますが、より簡潔でコンパクトなイメージを構築したいと考えています。

2. Golang ビルダーイメージの使用

Docker Hub は、Go 開発環境を備えた公式の Golang イメージ リポジトリを提供します。この Golang ビルダー イメージを直接使用して、アプリケーション イメージを構築できます。サードパーティのパッケージに大きく依存する一部の Go アプリケーションでは、この golang イメージをベース イメージとして使用して、専用のビルダー イメージをカスタマイズすることもできます。

golang:latest ベースイメージに基づいて golang-builder イメージをビルドします。 golang-builder イメージをビルドするために Dockerfile.build を記述します。

  1. // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.build
  2. golang :latest から
  3.  
  4. ワークディレクトリ /go/src
  5. httpserver.go をコピーします。
  6.  
  7. go build -o myhttpserver ./httpserver.go を実行します。

同じディレクトリに golang-builder イメージをビルドします。

  1. ビルドが成功すれば、Docker は再起動します。
  2. ビルド コンテキストを Docker デーモン送信5.895MB
  3. ステップ 1/4: golang:latestから
  4. ---> 1a34fad76b34  
  5. ステップ 2/4: WORKDIR /go/src
  6. ---> 2361824677d3  
  7. 中間コンテナ 01d8f4e9f0c4 を削除
  8. ステップ 3/4: httpserver.go をコピーします。
  9. ---> 1ff14bb0bc56  
  10. ステップ 4/4: go build -o myhttpserver ./httpserver.go を実行します。
  11. ---> 37a1b76b7b9e で実行中 
  12. ---> 2ac5347bb923  
  13. 中間コンテナ 37a1b76b7b9e を削除しています
  14. 2ac5347bb923 の構築に成功しました
  15. myrepo/golang-builder:latest のタグ付けに成功しました
  16.  
  17. リポジトリ タグ イメージ ID 作成サイズ 
  18. myrepo/golang-builder 最新 2ac5347bb923 3 分前 739MB

次に、golang-builder で構築された myhttpserver に基づいて最終的なアプリケーション イメージを構築します。

  1. # docker作成  --name appsource myrepo/golang-builder:latest  
  2. # docker cp appsource:/go/src/myhttpserver ./
  3. # docker rm -f アプリソース
  4. # docker rmi myrepo/golang-builder:latest
  5. ビルドが成功すると、リポジトリは更新されます。

このコマンドのロジックは、golang-builder イメージに基づいて起動されたコンテナ appsource からビルドされた myhttpserver をホストの現在のディレクトリにコピーし、その後、上記でビルドされた一時コンテナ appsource と golang-builder イメージを削除することです。最後の手順は最初の例と同じで、ローカル ディレクトリに構築された myhttpserver に基づいて最終イメージを構築します。便宜上、この一連のコマンドを Makefile に入れることもできます。

3. 小さめのアルプスの画像を使用する

ビルダー イメージは、最終的なアプリケーション イメージの重量を軽減するのに役立ちません。 myhttpserver イメージのサイズは 200 MB のままです。 「軽量化」するには、ベースイメージを小さくする必要があり、アルパインを選択しました。アルパイン画像のサイズは4M未満です。アプリケーションのサイズを追加すると、最終的なアプリケーション イメージのサイズは 20 MB 未満に縮小されると推定されます。

ビルダーイメージを結合するには、Dockerfile のベースイメージを alpine:latest に変更するだけです。

  1. // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.alpine
  2.  
  3. アルパインより:最新
  4.  
  5. コピー ./myhttpserver /root/myhttpserver
  6. chmod +x /root/myhttpserver を実行します。
  7.  
  8. ワークディレクトリ /root
  9. エントリポイント [ "/root/myhttpserver" ]

アルパイン バージョンのアプリケーション イメージをビルドします。

  1. ビルド 1.0.1 以降を実行している場合は、次のコマンドを実行します。
  2. ビルド コンテキストを Docker デーモン送信6.151MB
  3. ステップ 1/5: alpine:latestから
  4. ---> 053cde6e8953  
  5. ステップ 2/5: ./myhttpserver /root/myhttpserver をコピーします。
  6. --->ca0527a62d39  
  7. ステップ 3/5: chmod +x /root/myhttpserver を実行します。
  8. ---> 28d0a8a577b2 で実行中 
  9. ---> a3833af97b5e  
  10. 中間コンテナ 28d0a8a577b2 を削除しています
  11. ステップ 4/5: WORKDIR /root
  12. ---> 667345b78570  
  13. 中間コンテナを削除しています fa59883e9fdb
  14. ステップ 5/5: ENTRYPOINT /root/myhttpserver
  15. ---> adcb5b976ca3 で実行中 
  16. ---> 582fa2aedc64  
  17. 中間コンテナ adcb5b976ca3 を削除しています
  18. 582fa2aedc64 の構築に成功しました
  19. myrepo/myhttpserver-alpine:latest のタグ付けに成功しました
  20.  
  21. # Docker イメージ
  22. リポジトリ タグ イメージ ID 作成サイズ 
  23. myrepo/myhttpserver-alpine 最新 582fa2aedc64 4 分前 16.3MB

16.3MB、確かにサイズが小さくなりました!このイメージに基づいてコンテナを起動し、アプリケーションの実行に問題がないか確認します。

  1. # docker run myrepo/myhttpserver-alpine:latest
  2. standard_init_linux.go:185:実行 ユーザープロセスにより「そのようなファイルまたはディレクトリはありません」というメッセージが表示されました 

コンテナの起動に失敗しました。なぜ? alpine イメージは ubuntu 環境と同じではないためです。これについては以下で詳しく説明します。

2. 異種画像構築

私たちのイメージ ビルダー: myrepo/golang-builder:latest は、golang:latest イメージに基づいています。 golang ベースイメージには、Dockerfile-debain.template と Dockerfile-alpine.template の 2 つのテンプレートがあります。 golang:latest は Debian テンプレートに基づいており、Ubuntu と互換性があります。構築された myhttpserver には、動的共有リンク ライブラリに対して次の条件があります。

  1. # ldd myhttpserver
  2. linux-vdso.so.1 => (0x00007ffd0c355000)
  3. libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa8b36f000)
  4. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa8afa5000)
  5. /lib64/ld-linux-x86-64.so.2 (0x000055605ea5d000)

Debian ベースの Linux ディストリビューションは glibc を使用します。しかしアルパインは違います。 Alpine は musl libc の実装を使用します。したがって、上記のコンテナを実行すると、myhttpserver が依存する libc.so.6 が見つからないため、ローダーは終了に失敗します。

このようにビルド環境と実行環境が互換性のない状態を「異種イメージビルド」と呼びます。では、この問題をどう解決すればよいのでしょうか?続けましょう:

1. 静的ビルド

主流のプログラミング言語の中で、Go はすでに移植性の点で最高の言語の 1 つです。特に、ランタイムのすべての C コードが Go で書き直され、libc への依存が最小限に抑えられた Go 1.5 以降では、移植性がさらに向上しました。ただし、一部の機能では、C 実装と Go 実装の 2 つの実装バージョンがまだ提供されています。デフォルトでは、つまり CGO_ENABLED=1 の場合、プログラムとプリコンパイルされた標準ライブラリの両方が C 実装を使用します。この点に関する詳細な議論については、私の以前の記事「Go の移植性についても」を参照してください。ここでは詳細には触れません。したがって、異なる libc 実装を使用する Debian システムと Alpine システムの間には、当然ながら非互換性があります。この問題を解決するために、まず Go プログラムを静的にビルドし、次に静的にビルドされた Go アプリケーションを alpine イメージに配置することを検討します。

Dockerfile.build を変更し、Go ソース ファイルをコンパイルするときに CGO_ENABLED=0 を追加しましょう。

  1. // github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.build
  2.  
  3. golang :latest から
  4.  
  5. ワークディレクトリ /go/src
  6. httpserver.go をコピーします。
  7.  
  8. CGO_ENABLED=0 を実行して go build -o myhttpserver ./httpserver.go

このビルダーイメージをビルドします:

  1. ビルドが1.0.0.1の場合、Docker は.
  2. ビルドコンテキストを Docker デーモン送信4.096kB
  3. ステップ 1/4: golang:latestから
  4. ---> 1a34fad76b34  
  5. ステップ 2/4: WORKDIR /go/src
  6. ---> 593cd9692019  
  7. 中間コンテナ ee005d487ad5 を削除
  8. ステップ 3/4: httpserver.go をコピーします。
  9. ---> a095eb69e716  
  10. ステップ 4/4: CGO_ENABLED=0 を実行します go build -o myhttpserver ./httpserver.go
  11. ---> d9f3b3a6c36c で実行中 
  12. ---> c06fe8dccbad  
  13. 中間コンテナ d9f3b3a6c36c を削除しています
  14. c06fe8dccbad を正常に構築しました
  15. myrepo/golang- static -builder:latest のタグ付けに成功しました
  16.  
  17. # Docker イメージ
  18. リポジトリ タグ イメージ ID 作成サイズ 
  19. myrepo/golang- static -builder 最新 c06fe8dccbad 31 秒前 739MB

次に、golang-static-builder で構築された静的に接続された myhttpserver に基づいて、最終的なアプリケーション イメージを構築します。

  1. # docker作成  --name appsource myrepo/golang-static-builder:latest  
  2. # docker cp appsource:/go/src/myhttpserver ./
  3. # ldd myhttpserver
  4. 動的実行ファイルはない
  5. # docker rm -f アプリソース
  6. # docker rmi myrepo/golang- static -builder:latest
  7. ビルド 1.0.1 以降を実行している場合は、次のコマンドを実行します。

新しいイメージを実行します。

  1. # docker run myrepo/myhttpserver-alpine:latest
  2. ウェブサーバーの起動
  3. ->ポート:1111リッスン

注: strace を使用すると、Go が静的にリンクするときに独自のランタイム実装のみを使用し、libc.a のコードを使用しないことを証明できます。

  1. # CGO_ENABLED=0 strace -f go build httpserver.go 2>&1 | grepオープン| grep -o '/.*\.a' > go- static -build-strace-file- open .txt

go-static-build-strace-file-open.txt ファイルを開き、その内容を確認します。 libc.a ファイルは見つかりません (Ubuntu では、libc.a は通常 /usr/lib/x86_64-linux-gnu/ にあります)。これは、go build が libc.a ファイルを開いてその中のシンボル定義を取得しようとしなかったことを意味します。

2. alpine golangビルダーを使用する

私たちの Go アプリケーションは、alpine ベースのコンテナーで実行されます。 alpine golang ビルダーを使用してアプリケーションをビルドできます (静的リンクなし)。前述したように、golang には alpine テンプレートがあります。

  1. リポジトリ タグ イメージ ID 作成サイズ 
  2. golang alpine 9e3f14138abd 7日前 269MB

golang ビルダーの alpine バージョンの Dockerfile の内容は次のとおりです。

  1. //github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.alpine.build
  2.  
  3. golang:alpineより
  4.  
  5. ワークディレクトリ /go/src
  6. httpserver.go をコピーします。
  7.  
  8. go build -o myhttpserver ./httpserver.go を実行します。

以降の操作は、前の golang ビルダーと同じです。alpine golang ビルダーを使用してアプリケーションをビルドし、alpine イメージに配置します。ここでは詳細には触れません。

3. 多段階イメージ構築:開発者エクスペリエンスの向上

Docker 17.05 より前では、上記のようにイメージを構築していました。異種イメージ ビルダー モードを使用する場合でも、2 つの Dockerfile を維持し、コンテナーからのアプリケーションのコピー、ビルド コンテナーのクリーンアップ、イメージのビルドなど、docker build コマンドの外部でいくつかの操作を実行する必要があることがわかります。 Docker コミュニティはこの問題を認識し、マルチステージ イメージ構築メカニズム (マルチステージ) を実装しました。

まず、上記の例のマルチステージ ビルドに使用される Dockerfile を見てみましょう。

  1. //github.com/bigwhite/experiments/multi_stage_image_build/multi_stages/Dockerfile  
  2. golang:alpineからビルダーとして 
  3. ワークディレクトリ /go/src
  4. httpserver.go をコピーします。  
  5. go build -o myhttpserver ./httpserver.go を実行します。  
  6. アルパインより:最新 
  7. ワークディレクトリ /root/
  8. --from=builder /go/src/myhttpserver にコピーします。  
  9. chmod +x /root/myhttpserver を実行します。  
  10. エントリポイント [ "/root/myhttpserver" ]

この Dockerfile の内容を読んだ後の第一印象は、前の 2 つの Dockerfile が結合され、各 Dockerfile が個別の「ステージ」になっているということです。確かにその通りですが、この Docker には、さまざまな「ステージ」間の接続を確立するための新しい構文形式もいくつかあります。このような Dockerfile については、次の点を知っておく必要があります。

  • マルチステージ ビルドをサポートする Dockerfile は、複数のビルド ステージ間に内部接続を確立し、次のビルド ステージで前のビルド ステージの成果物を使用できるようにして、ビルド ステージのチェーンを形成します。
  • マルチステージ ビルドの最終結果は 1 つのイメージのみであり、複数の冗長な一時イメージまたは一時コンテナー オブジェクトの生成を回避します。これはまさに必要なことであり、必要なのは結果だけです。

上記の例をマルチステージを使用して構築してみましょう。

  1. ビルドが 0x80000000 の場合、このコマンドは実行されません。
  2. ビルドコンテキストを Docker デーモン送信3.072kB
  3. ステップ 1/9: golang:alpineからビルダーとして
  4. ---> 9e3f14138abd  
  5. ステップ 2/9: WORKDIR /go/src
  6. ---> キャッシュの使用 
  7. ---> 7a99431d1be6  
  8. ステップ 3/9: httpserver.go をコピーします。
  9. ---> 43a196658e09  
  10. ステップ 4/9: go build -o myhttpserver ./httpserver.go を実行します。
  11. ---> 9e7b46f68e88 で実行中 
  12. ---> 90dc73912803  
  13. 中間コンテナ 9e7b46f68e88 を削除しています
  14. ステップ 5/9: alpine:latestから
  15. ---> 053cde6e8953  
  16. ステップ 6/9: WORKDIR /root/
  17. ---> キャッシュの使用 
  18. ---> 30d95027ee6a  
  19. ステップ 7/9: COPY --from=builder /go/src/myhttpserver.  
  20. ---> f1620b64c1ba  
  21. ステップ 8/9: chmod +x /root/myhttpserver を実行します。
  22. ---> e62809993a22 で実行中 
  23. ---> 6be6c28f5fd6  
  24. 中間コンテナ e62809993a22 を削除
  25. ステップ 9/9: ENTRYPOINT /root/myhttpserver
  26. ---> e4000d1dde3d で実行中 
  27. ---> 639cec396c96  
  28. 中間コンテナ e4000d1dde3d を削除
  29. 639cec396c96 の構築に成功しました
  30. myrepo/myhttserver-multi-stage:latest のタグ付けに成功しました
  31.  
  32. # Docker イメージ
  33. リポジトリ タグ イメージ ID 作成サイズ 
  34. myrepo/myhttserver-multi-stage 最新 639cec396c96 約1時間前 16.3MB

この画像を実行してみましょう:

  1. # docker run myrepo/myhttserver-multi-stage:latest
  2. ウェブサーバーの起動
  3. ->ポート:1111リッスン

IV.まとめ

マルチステージイメージ構築により、開発者は Dockerfile を使用して一度に小さなイメージをより簡単に構築できるようになり、優れたユーザーエクスペリエンスが提供され、CI/CD などの自動化システムへの接続が容易になります。ただし、現在、マルチステージ ビルドは Docker 17.05 以降のバージョンでのみサポートされています。この機能を学習して練習したいが環境がない場合は、play-with-docker が提供する実験環境を使用できます。

Dockerラボで遊ぶ

<<:  恒源智成とアリババクラウドが共同でビジネスチェーン全体をカバーするダブル11クラウドカーニバルを開始

>>:  SaaS 無料トライアルの 3 つの秘密

推薦する

アリババのオンライン旅行業界のスタートアップはプラットフォーム電子商取引モデルを推進している

4月、長江南部は雨が多かったが、同城網の呉志祥CEOの気分は落ち込んでいなかった。Ctrip.com...

B2CプラットフォームSEMでキーワードを見つける方法

B2C プラットフォーム SEM のキーワードを見つけて設定する方法は、SEM スペシャリストにとっ...

SEO検索エンジン最適化のヒント

メディアのインタビュー記事の要約の中で、Google のスタッフは、一般的にウェブサイトのランキング...

「怠け者」:ウサギを待って友好的な関係を築く方法

みなさんこんにちは。私はHongtu Internetです。私たちが構築したウェブサイトは、数か月前...

ローカルポータルサイトのチャンネルページの最適化方法の簡単な分析

地域不動産情報ポータルの主要チャネルは、ウェブサイトの中核です。各チャネルはウェブサイトに検索トラフ...

クラウド移行の8つの課題

クラウド コンピューティングは成熟度が高まっていますが、それをより困難にしたり、コストを増大させたり...

SEOの観点からドメイン名の選び方とそれに応じたテクニックを分析

著者は以前、ウェブサイトでは高品質のコンテンツと安定した外部リンクを持つことに加え、いくつかの細部に...

webmaster.comからの毎日の報告:24quanは苦情のために閉鎖され、速達倉庫の爆発は避けられません

1. 24quanは苦情により営業停止となったが、来週には営業を再開する可能性がある。突然の営業停止...

zjiの3回線サーバーの簡単なレビュー、香港アリババクラウドの専用サーバーのレビュー

「Zji Hong Kong物理マシン:1台のマシンに3つの回線、Alibaba専用回線+Huawe...

Tongcheng.comの生存物語:巨人と交通渋滞との戦い

巨人のプレッシャーの下でどうやって生き残るか? Wu Zhixiang と Tongcheng.co...

医療業界は「微博をコントロールする」から「微博をコントロールする」まで、ほんの数ステップで済む。

バスの中で携帯電話を手に持ち、自分の生活をライブストリーミングしている男女をよく見かけます。彼らは典...

ウェブサイトの革新の本質を理解するために基本に戻る

ウェブサイトは、ユーザーに覚えてもらうために何かユニークなものを提供する必要があります。同時に、ユー...

JD.comとSuningが対決を覆す:中小電子商取引企業は価格戦争の傍観者となる

【はじめに】一部の投資家は、電子商取引業界には深刻な問題があり、主力事業は利益が出ず、誰もが資金調達...

今すぐクラウドでゲームをプレイし、月額999ルーブルを支払って、最先端のGeForceテクノロジーを手に入れましょう

本題とは関係のない話をしましょう。GFN は主にさまざまな大規模ゲームをクラウドに載せているロシアの...