Goでのネットワークサービスの記述は非常に簡単です。標準ライブラリには多くのツールがあり、不足しているものがあれば、Githubにはほとんどのニーズを満たすためのトレンディなライブラリがたくさんあります。
しかし、同じインフラストラクチャで動作するさまざまなサービスについて書く必要がある場合はどうでしょうか?
すべての悪魔がすべての新鮮で多様なスムージーを使用する場合、新しい機能を追加することは言うまでもなく、テクノロジーは維持するのが難しく、高価な「動物園」をもたらします。
Badooには、さまざまな言語で書かれた30を超えるセルフスタイルのデーモンがあり、そのうちの10個がGoにあります。 これらのデーモンはすべて、約300台のサーバーで動作します。 最後に「動物園」を取得せずに、どうやってスムージー、開発者、QA、およびリリースに誰も制限せずに平和的に眠る方法を管理者がどのように管理しているのか、まだ口論をしていない
Badoo Goが最初に登場したのは2014年頃で、ユーザーの座標間の交差点を探すデーモンをすばやく作成するタスクに直面したときです。 その後、Goの最新バージョンは1.3であったため、長い間GCの一時停止に苦労しました。これについては、オフィスでのGoミーティングで説明しました。
それ以来、Goで書かれたデーモンが増えています。 基本的に、これは以前はCで記述されていたものですが、PHPではうまく機能しない場合があります(たとえば、 非同期プロキシや「クラウド」のリソーススケジューラー )。
クライアントアプリケーションからのすべてのリクエストはPHPによって処理され、PHPはさまざまな自己記述型および非自己記述型のサービスに送られます。 簡略化すると、次のようになります。
このルールにはいくつかの例外がありますが、一般的に、Goのすべての悪魔については、次のことが当てはまります。
- 彼らはインターネットに出ない
- デーモンの主な「ユーザー」はPHPコードです。
私たちのすべての悪魔は、執筆の言語に関係なく、プロトコル、ログ、統計、展開などの点で「外部から」同じように見えます。 これにより、管理者、リリースエンジニア、QA、PHP開発者の作業が楽になります。
したがって、一方では、Goデーモンに使用する多くのアプローチは、C / C ++でデーモンを作成する既存のアプローチによって決定され、他方では、この記事の多くはGoだけでなく、すべてのデーモンにも当てはまります。
以下では、基本的なインフラストラクチャパーツがどのように配置され、Goコードにどのように反映されるかを説明します。
プロトコル
クライアントとサーバーの相互作用に関しては、プロトコルに関して疑問が生じます。 Google Protobufを基盤として採用しました。 私たちのプロトコルはgRPCの簡易バージョンに似ているため、「車輪を再発明する必要があるのはなぜですか」というカテゴリから繰り返し質問されました。 ほとんどの場合、今日はgRPCを実際に使用しますが、当時(2008年)はまだ存在していなかったため、相互に交換する意味がありません。
Protobufは、メッセージ本文をバイナリ表現でラップするだけで、そのタイプは保持しません。 したがって、クライアントがリクエストを使用してサーバーにアクセスするたびに、サーバーは、どのprotobufメッセージであるか、どのメソッドを実行する必要があるかを理解する必要があります。 これを行うには、protobufメッセージの前にメッセージタイプ識別子とメッセージ長を追加します。 識別子により、どのGPBメッセージが到着したかが明確になり、長さにより、必要なサイズのバッファをすぐに割り当てることができます。
その結果、プロトコルに関する1つの呼び出しは次のようになります。
- 4バイト-メッセージ長N( ネットワークバイト順の数);
- 4バイト-メッセージタイプ識別子(も);
- Nバイト-メッセージ本文。
クライアントとサーバーの両方がメッセージ識別子について同じ情報を持つために、特別な名前request_msgid
とresponse_msgid
持つenumの形式でそれらをプロトタイプファイルに保存します。 例:
enum request_msgid { REQUEST_RUN = 1; REQUEST_STATS = 2; } // request response, enum response_msgid { RESPONSE_GENERIC = 1; // , response , RESPONSE_RUN = 2; RESPONSE_STATS = 3; } message request_run { // ... } message response_run { // ... } message request_stats { // ... } // ...
gpbrpcと呼ばれるライブラリは、このすべてのプロトコル部分を担当します。 大まかに2つの部分に分けることができます。
- 1つは、
request_msgid
とresponse_msgid
基づいてresponse_msgid
一致マップid =>メッセージ、ハンドラーメソッドのテンプレート、型キャストによる呼び出しのマップ、および必要な他のコードを生成するコードジェネレーターです。 - 2番目の部分では 、ネットワーク経由で送信されたこのデータをすべて解析し 、対応するハンドラーメソッドを呼び出します。
最初の部分のコードジェネレーターは、Google Protobufのプラグインとして実装されます。
上記のprotoファイル用に自動生成されたハンドラーは次のようになります。
// , , proto- type GpbrpcInterface interface { RequestRun(rctx gpbrpc.RequestT, request *RequestRun) gpbrpc.ResultT RequestStats(rctx gpbrpc.RequestT, request *RequestStats) gpbrpc.ResultT } func (GpbrpcType) Dispatch(rctx gpbrpc.RequestT, s interface{}) gpbrpc.ResultT { service := s.(GpbrpcInterface) switch RequestMsgid(rctx.MessageId) { case RequestMsgid_REQUEST_RUN: r := rctx.Message.(*RequestRun) return service.RequestRun(rctx, r) case RequestMsgid_REQUEST_STATS: r := rctx.Message.(*RequestStats) return service.RequestStats(rctx, r) } } // - /* func ($receiver$) RequestRun(rctx gpbrpc.RequestT, request *$proto$.RequestRun) gpbrpc.ResultT { // ... } func ($receiver$) RequestStats(rctx gpbrpc.RequestT, request *$proto$.RequestStats) gpbrpc.ResultT { // ... } */
gogo / protobuf
Protobufのドキュメントでは、Goでこのライブラリを使用することをお勧めします。 しかし、残念なことに、不正なGCコードが生成されます。 ドキュメントの例:
message Test { required string label = 1; optional int32 type = 2 [default=77]; }
になります
type Test struct { Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` }
構造の各フィールドはポインターになっています。 これはoptional
フィールドに必要optional
フィールドでは、フィールドが存在しない場合と、フィールドにゼロ値が含まれている場合を区別する必要があります。
不要な場合でも、生成されたコード内のポインターの存在をライブラリで制御することはできません。 しかし、すべてがそれほど悪いわけではありません。彼女はそれがサポートされているgogoprotobufのフォークを持っています。 これを行うには、プロトファイルで適切なオプションを指定する必要があります。
message Test { required string label = 1 [(gogoproto.nullable) = false]; optional int32 type = 2 [(gogoproto.nullable) = false]; }
ポインターを取り除くことは、GCの一時停止がはるかに長かったバージョン1.5までGoで特に当てはまりました。 ただし、現在でも、ロードされたサービスのパフォーマンスを大幅に (場合によっては) 向上させることができます。
nullable
に加えて、ライブラリでは、パフォーマンスと結果のコードの利便性の両方に影響する他の多数の生成オプションを追加できます。 たとえば、 gostring
は、Go構文の値で現在の構造を保存します。これは、デバッグやテストの作成に便利です。
オプションの適切なセットは、特定の状況によって異なります。 ほとんどの場合、少なくともnullable
、 sizer_all, unsafe_marshaler_all
、 unsafe_unmarshaler_all
ます。 ところで、接尾辞_all
が付いたオプションを持つオプションは、フィールドごとに複製することなく、ファイル全体にすぐに適用できます。
option (gogoproto.sizer_all) = true; message Test { required string label = 1; optional int32 type = 2; }
ジョンソン
Google Protobufでは、ほとんどすべてが問題ありませんが、バイナリプロトコルであるため、デバッグするのは困難です。
完成したクライアントとサーバー間の相互作用のレベルで問題を見つける必要がある場合、たとえばWiresharkの gpbs-dissectorを使用できます。 ただし、これは、クライアントまたはサーバーがまだない新しい機能を開発する場合には適していません。
実際、ある種のテスト要求をサービスに書き込むには、それをバイナリメッセージにラップできるクライアントが必要です。 デーモンごとにこのようなテストクライアントを作成することは、それほど難しくはありませんが、不便で日常的な作業です。 したがって、gpbrpcは、gpbインターフェースとは異なるポートでプロトコルのJSONのような表現を処理できます。 これに対するバインディング全体が自動的に生成されます(protobufについて上記で説明した方法と同様の方法で)。
その結果、コンソールでテキスト形式でリクエストを作成し、すぐに回答を得ることができます。 デバッグに便利です。
pmurzakov@shell1.mlan:~> echo 'run {"url":"https://graph.facebook.com/?id=http%3A%2F%2Fhabrahabr.ru","task_hash":"a"}' | netcat xtc1.mlan 9531 run { "task_hash": "a", "task_status": 2, "response": { "http_status": 200, "body": "{\"og_object\":{\"id\":\"627594553918401\",\"description\":\" – , . , , – IT- .\",\"title\":\" / \",\"share\":{\"comment_count\":0,\"share_count\":2456},\"id\":\"http:\\/\\/habrahabr.ru\"}", "response_time_ms": 179 } }
答えが膨大で、そこから一部を選択する必要がある場合、または何らかの形で変換する必要がある場合は、 jqコンソールユーティリティを使用できます。
構成
一般に、構成には通常2つの基本要件があります。
- 読みやすさ;
- 構造を説明する機会。
このためにまだ新しいエンティティを導入しないために、すでにあるものを利用しました:protobuf-構造、可読性-そのJSON表現。
デバッグ用のJSON表現用のprotobufおよびパーサージェネレーターのすべてのバインディングが既にあります(前のセクションを参照)。 あとは、構成用のプロトタイプファイルを追加して、このパーサーに「フィード」するだけです。
実際、すべてが少し複雑です。異なるデーモンを可能な限り標準化することが重要であるため、構成にはすべてのデーモンに同じ部分があります。 これは、一般的なprotobufメッセージによって記述されます。 最終的な構成は、標準化された部分と特定のデーモンに固有のものの組み合わせです。
このアプローチは埋め込みに適しています:
type FullConfig struct { badoo.ServiceConfig yourdaemon.Config }
わかりやすくするための例。 共通部分:
message service_config { message daemon_config_t { message listen_t { required string proto = 1; required string address = 2; optional bool pinba_enabled = 4; } repeated listen_t listen = 1; required string service_name = 2; required string service_instance_name = 3; optional bool daemonize = 4; optional string pid_file = 5; optional string log_file = 6; optional string http_pprof_addr = 7; // net/http/pprof + expvar address optional string pinba_address = 8; // ... } }
特定のデーモンの一部:
message config { optional uint32 workers_count = 1 [default = 4000]; optional uint32 max_queue_length = 2 [default = 50000]; optional uint32 max_idle_conns_per_host = 4 [default = 1000]; optional uint32 connect_timeout_ms = 5 [default = 2000]; optional uint32 request_timeout_ms = 7 [default = 10000]; optional uint32 keep_alive_ms = 8 [default = 30000]; }
その結果、JSON設定は次のようになります。
{ "daemon_config": { "listen": [ { "proto": "xtc-gpb", "address": "0.0.0.0:9530" }, { "proto": "xtc-gpb/json", "address": "0.0.0.0:9531" }, { "proto": "service-stats-gpb", "address": "0.0.0.0:9532" }, { "proto": "service-stats-gpb/json", "address": "0.0.0.0:9533" }, ], "service_name": "xtc", "service_instance_name": "1.mlan", "daemonize": false, "pinba_address": "pinbaxtc1.mlan:30002", "http_pprof_addr": "0.0.0.0:9534", "pid_file": "/local/xtc/run/xtc.pid", "log_file": "/local/xtc/logs/xtc.log", }, // "workers_count": 4000, "max_queue_length": 50000, "max_idle_conns_per_host": 1000, "connect_timeout_ms": 2000, "handshake_timeout_ms": 2000, "request_timeout_ms": 10000, "keep_alive_ms": 30000, }
listen
は、デーモンがリッスンするすべてのポートをリストします。 タイプxtc-gpb
およびxtc-gpb/json
の最初の2つの要素は、そのgpbrpc
のポートとそのJSON表現用です。 また、 service-stats-gpb
およびservice-stats-gpb/json
て、統計情報を収集します。これについては後で説明します。
統計
統計を収集する場合(構成の標準化された部分の場合)、各デーモンが少なくとも基本的なメトリックを提供することが重要です:処理された要求の数、CPUとメモリの消費、ネットワークトラフィックなど。このタイプの統計はservice-stats-gpb
ポートから収集されますservice-stats-gpb
、それはすべての悪魔に同じです。
実際、統計の要求は、デーモンへの通常の要求と変わらないため、同じアプローチを使用します。統計は、通常の要求と同様にgpbrpcの観点から説明されます。 この「標準化された」統計のハンドラーはすでにフレームワークにあるため、次のデーモンのために毎回記述する必要はありません。
構成との類推により、すべてのデーモンの同じ統計に加えて、各デーモンも特定のデーモンを提供できます。
1分ごとに、PHPクライアント統計コレクターはデーモンに接続し、値を要求して時系列ストアに保存します。 これらのデータに基づいて、このようなグラフを作成します。
最初の5つのグラフの値はすべてのデーモンから自動的に収集され、残りは特定のデーモンに固有の値です。
一般的に、統計情報が多すぎることはないと考えており、データの最大量を取得しようとしています。そのため、後で問題や変更に対処するのが容易になります。 したがって、configでpinba_address
パラメーターを確認できます。これは、デーモンが統計情報を送信するPinbaサーバーのアドレスです。
ピンバから、応答時間分布グラフを作成します。
デバッグとプロファイリング
設定でも、パラメータhttp_pprof_addr
気付くことができます。 これはnet / http / pprof portで、Goの組み込みツールであり、コードのプロファイルを簡単に作成できます。 彼については多くの記事が書かれているので(たとえば、 thisやthis )、彼の作品の詳細については触れません。
本番デーモンのビルドでもpprofを残します。 使用するまでオーバーヘッドはほとんどありませんが、柔軟性が追加されます。いつでも接続して、何が発生してどのリソースが消費されているかを把握できます。
さらに、 expvarを使用して、HTTP経由で任意のデーモン変数のアクセス可能な値を1行のコードのJSON形式で作成できるようにします。
expvar.Publish("varname", expvar.Func(func() interface{} { return somevariable }))
デフォルトでは、 expvar
HTTPハンドラーexpvar
DefaultServeMux
追加され、 http:// yourhost / debug / varsで利用可能です。 接続すると、パッケージに副作用があります。 runtime.ReadMemStats()
結果だけでなく、バイナリが起動されたコマンドラインをすべてのパラメーターで自動的に公開します。
ご注意 ReadMemStats()は、大量のメモリが割り当てられた場合、長い間世界を停止する可能性があります。 同僚のMarko Kevacがこのトピックに関するチケットを作成しましたが、バージョン1.9ではこれを修正する必要があります。
標準値に加えて、すべてのデーモンは多くのデバッグ情報を公開しています。最も重要なのは次のとおりです。
- デーモンの起動に使用する構成。
- 上記で書いたすべての統計カウンターの値。
- バイナリの収集方法に関するデータ。
最初の2つのポイントでは、すべてが明確だと思います。 後者は、デーモンがビルドされたGoのバージョン、gitコミットのハッシュ、ビルド時間、およびビルドに関するその他の有用なデータに関する情報を提供します。
例:
これを行うには、version.goファイルを作成するときに生成します。このファイルにはすべての情報が書き込まれます。 ただし、 ldflags -Xを使用しても同様の効果が得られます。
ログ
ロガーとして、カスタムフォーマッタでlogrusを使用します。 RsyslogおよびLogstashを使用したログファイルはElasticsearchで収集され、その後Kibana ダッシュボード (ELK)に表示されます。
これらのソリューションを選択した理由、およびログアセンブリのその他の詳細については、 この記事で既に説明したので、繰り返しません。
ワークフロー、テストなど
すべての作業はJIRAで行われます。 各チケットは個別のブランチです。 TeamCityは、チケットブランチごとにバイナリを収集します。 アセンブリには、GNU Makeを使用します。これは、直接コンパイルすることに加えて、proto-filesからversion.goとgpbrpcのコードを生成する必要があり、いくつかのタスクも実行するためです。
Go 1.5 Vendor Experimentを使用して、ベンダーディレクトリに依存コードを配置できます。 しかし残念ながら、今のところ、すべての依存ファイルをリポジトリに追加するだけでこれを行っています。 販売には何らかのユーティリティを使用する計画があります。 Depは有望に見えますが、安定化を待つだけです。
チケットがレビューに合格すると、QAチームのメンバーがチケットを受け取ります。 デーモンの新機能の機能テストを作成し、リグレッションをチェックします。 テストはPHPで記述されています。ほとんどの場合、実稼働環境のデーモンのクライアントは彼であるためです。 したがって、テストで何かが機能する場合、実稼働でも機能することを保証します。
Goのテストに関しては、オプションであり、必要に応じて記述されています。 しかし、私たちはそれに取り組んでおり、Goでさらにテストを書く予定です(追記を参照)。
QAによって検証され、リリースの準備ができているチケットから、TeamCityはビルドブランチを収集します。 計算の準備ができたら、開発者は特別なインターフェイスに入り、終了します。 同時に、ビルドブランチがマスターにマージされ、管理者のJIRAプロジェクトに計算用のチケットが作成されます。
鬼柄
私たちの条件で新しいデーモンを書くのは簡単ですが、それでもいくつかのテンプレートアクションが必要です:ディレクトリ構造を作成し、クライアントサーバープロトコルのプロトファイルを作成し、そのための設定ファイルとプロトファイルを作成し、gpbrpcに基づいてサーバー開始コードを書く必要があります。 。 毎回これに煩わされないように、小さなbashスクリプトがあるテンプレートデーモンを使用してリポジトリを作成しました。これにより、このテンプレート用の新しい本格的なデーモンが作成されます。
おわりに
その結果、新しいデーモンでコードが1行も記述されていない場合でも、インフラストラクチャの準備がすでに整っていることがわかりました。
- 同じ方法で構成可能。
- 統計とログを書き込みます。
- 他のデーモン(他の言語で書かれたものを含む)と同じ生産的(protobuf)および読みやすい(JSON)プロトコルを使用して通信します。
- 等しく維持および監視されます。
これにより、開発者のリソースを無駄にすることなく、予測可能で簡単にサポートされるサービスを取得できます。
それだけです Goでどのように悪魔を書きますか? コメントへようこそ!
PSこの機会に私たちは才能を探していると言います。
Goにはすでに多くのことが書かれていますが、この分野を発展させたいので、まだ書かれていないところがあります。 そして、私たちはこれを手伝ってくれる知的な人を探しています。
Go-developerであり、C / C ++を少し知っている場合は、PMまたはメールでmkevacを書いてください :m.kevac@corp.badoo.com(彼のチームの同僚を見てください)。