dockerの出現により、監視サービスとしての私たちの生活はもう少し複雑になりました。 前に書いたように 、サービスの機能の1つはサービスの自動検出です。つまり、エージェントはサーバー自体で実行されているサービスを見つけ、その構成を読み取り、メトリックの収集を開始します。
しかし、実稼働のある時点で、ドッカーがお客様に現れ始め、自動検出が機能しなくなりました。 dockerを介して起動されるプロセスには、さまざまな名前空間(mnt、net、user、pid)が付加されます。これにより、コンテナーの外部からの作業とコンテナー内のネットワークが大幅に複雑になります。
カットの下で、この問題をどのように解決したか、どのオプションを試したか、最終的には何が機能したかを説明します。
タスクは2つの部分に分けることができます。
- コンテナ内のファイルを読むことを学ぶ
- コンテナで実行されているサービスとネットワークを介して接続することを学ぶ
コンテナ内のファイル
最初の仮説は非常に単純でした。fsコンテナがホストディスク上のどこにあるかを単純に判断し、パスを変更してそこに行きます。 残念ながら、これはAUFSの場合にのみ機能しますが、本番環境ではほとんど見られません。
さらに、エージェントコードから直接MNT名前空間にsetnを作成しようとしましたが、これも機能しませんでした。 実際、mnt(およびユーザーも)名前空間の設定は、シングルスレッドアプリケーションによってのみ実行できます。
プロセスがマルチスレッドの場合、プロセスは新しいマウント名前空間に再関連付けされない場合があります
エージェントはgolangで記述されており、setnsを呼び出したいときには、goshランタイムはすでにいくつかのスレッドを生成しています。 エージェントがnsenterのような特別なプロセスを開始できるように、最初にそれらをクライアントマシンにドラッグする必要がありますが、これは実際には必要ありませんでした。
docker exec -ti
を介して何かを実行するオプションがありましたが、まず、このコマンドはバージョン1.3からのみ使用できます。次に、dockerだけでなく、他のコンテナー化サービスもあります。猫でさえありません。
次に、goの興味深いハックを見つけました。これにより、goランタイムを実行する前にシステムコンストラクターでsetnsを作成できます。 その結果、エージェントは特定の引数で起動し、目的のnsでファイルを読み取ったり、コンテナファイルシステムのglobを展開したりできるなどの結論に達しました。 しかし、setnsはCコードで実行する必要があるため、Cで記述して起動引数を処理する必要がありました。 そして、呼び出しの時に
__attribute__((constructor))
argv / argcはまだ初期化されていないため、引数を読んで/proc/self/cmdline
から起動する必要がありました。
エージェントがこのモードで起動すると、その作業の結果がstdout / stderrにダンプされ、親エージェントがこれを読み取ります。 読み取り可能なファイルのサイズに別の制限を加える必要がありました。ディスクをロードしないために、クライアントサーバーにディスクを顕著にロードできるため、200KB(多くの場合、geoipマッピングの重いnginx構成があります)を超えるファイルを読み取ろうとはしませんでした。
このアプローチは、ファイルを1回読み取る必要がある場合にのみ有効ですが、たとえば、テールログが必要な場合は機能しません。 一方、パフfsコンテナーのログは通常書き込まれません。 通常、これらはdocker stdout / stderrにラップされてdockerdを実行するか、ホストfsのマウントされたパーティションに書き込まれます。
dockerdオプションはまだ処理していませんが、クライアントでは珍しいことは注目に値します。 ログの大規模なストリームで、dockerdがプロセッサのロードを開始するという事実が原因のようです。
ログ用にマウントされたディレクトリの場合、 docker inspect
からの情報を介してホストのfsで必要なファイルを見つけようとしています。そのようなログを解析したいプラグインは、既にコンテナー外のファイルへのパスを取得します。
コンテナネットワーク
コンテナ内のサービスを使用してネットワークを介して作業する方法についての最初のアイデアも素朴でした。コンテナのIPをdocker inspect
から取得して操作します。 次に、ホストからコンテナネットワークへのアクセスがまったくない可能性があることが判明しました(macvlan)。 さらに、lxcなどがあります。
私たちは定住に向かうことにしました。 ネットワーク名前空間は、mntやユーザーとは異なり、1つの特定のアプリケーションスレッドに対して再定義できます。 golangでは、一見、これは非常に簡単です。
しかし、すべてがより複雑であることが判明しました。 実際、スレッドがブロックされると、ランタイムはこのゴルーチンの実行がこのスレッドに留まることを保証しません。 「 Linux Namespaces And Go Do n't Mix 」の投稿には、まさにそのようなケースの良い説明があります。
最初は、setnsを使用してロックされたスレッドでコンテナ内のサービスを監視するプラグインを起動する予定でしたが、これは非常に最初のhttpクライアントで破損しました。
go スケジューラーに影響を与える能力がないため、新しいスレッドの生成につながらないコードのみをスレッドに残す方法を探し始めました。
setnsの直後にtcp接続を確立すると、100%のケースでパスし、後で名前空間を終了してスレッドのロックを解除すると、開いている接続が引き続き機能することに気付きました(これがなぜ機能するのか説明するのは難しいと思います)。
その後、タスクは、 Dialer
(TCP接続を担当する機能)を、監視するさまざまなサービスを操作するためのすべてのライブラリとスリップさせることになりました。
- redis :
client := redis.NewClient(&redis.Options{ Dialer: func() (net.Conn, error) { return utils.DialTimeoutNs("tcp", params.Address, params.NetNs, redisTimeout) }, ReadTimeout: redisTimeout, WriteTimeout: redisTimeout, Password: params.Password, })
- memcachedの場合、ライブラリは使用しませんが、手動でtcpを操作するため、問題もありませんでした。
- rabbitmqでは、httpに移動します。標準のhttpクライアントはカスタムダイヤルを受け入れることができます
- mysql :
mysql.RegisterDial("netns", func(addr string) (net.Conn, error) { return utils.DialTimeoutNs("tcp", addr, params.NetNs, connectTimeout) }) db, err = sql.Open("mysql", fmt.Sprintf("netns(%s)/?timeout=%s&readTimeout=%s&writeTimeout=%s", params.Address, connectTimeout, readTimeout, writeTimeout))
- c postgresqlは非常に松葉杖であることが判明したため、
database/sql
用の独自の擬似ドライバを作成する必要がありました:
func init() { sql.Register("postgres+netns", &drv{}) } type drv struct{} type nsDialer struct { netNs string } func (d nsDialer) Dial(ntw, addr string) (net.Conn, error) { return utils.DialTimeoutNs(ntw, addr, d.netNs, connectTimeout) } func (d nsDialer) DialTimeout(ntw, addr string, timeout time.Duration) (net.Conn, error) { return utils.DialTimeoutNs(ntw, addr, d.netNs, timeout) } func (d *drv) Open(name string) (driver.Conn, error) { parts := strings.SplitN(name, "|", 2) netNs := "" if len(parts) == 2 { netNs = parts[0] name = parts[1] } return pq.DialOpen(nsDialer{netNs}, name) }
次に、ドライバーを呼び出します。
dsn := fmt.Sprintf("%s|postgres://%s:%s@%s/%s", p.NetNs, p.User, p.Password, p.Address, dbName) db, err := sql.Open("postgres+netns", dsn)
合計
振り返ってみると、setnsでオプションを選択したことを後悔していなかったため、lxcを使用するクライアントに対して同じコードが最近完全に機能しました。
現在閉じられていない唯一のサービスはコンテナ内のjvmですが、これはまったく別の話です。