2024.12.19
システムの穴を運用でカバーしようとしてミス多発… バグが大量発生、決算が合わない状態から業務効率化を実現するまで
Go編「Go ルーチンで並列処理を実装しよう」(全1記事)
リンクをコピー
記事をブックマーク
森下篤氏(以下、森下):私のところでは、Golangについて説明します。弊社は会社名もGOなので非常にわかりにくいのですが、基本的には大文字で「GO」と書くと会社を示すことが多いです。(スライドを示して)小文字の「Go」やスライドのアイコンを使った時には、言語のGoと思ってもらえると助かります。
まず、Goでの並列・並行処理について話します。独自の軽量スレッドであるGoルーチンというものがあって、Goルーチン同士の同期・非同期通信の仕組みであるチャネルが構文に含まれています。
Goランタイム自体は、OSのスレッドを隠蔽し、Goルーチンを時分割に割り当てて動かします。割り当てのスケジューリング自体はGoランタイムに任されていて、指示ができません。
つまり、Goルーチンを起動したからといって、それをすぐに並列処理で実行してくれるというわけではありません。そしてGoルーチンは、OSのスレッドを管理するよりもかなり軽量なため、使い捨てができます。
(スライドを示して)下に書いてあるものが簡単に描いてみた図ですが、プログラミングを実装している、Goを実装している人にとっては、複数のGoルーチンをたくさん起動するという概念しか扱うことができなくて、これを実際にCPU上で実行する、どのGoルーチンを実行するかというのは、Goランタイムに任されているというのがGoの並列処理のGoルーチンの考え方です。
森下:では、実際にGoルーチンの実行方法を説明します。go 関数()で実行します。すごくシンプルです。即時関数を使うと、go func(){ }()というかたちになります。
(スライドを示して)右側がその実装例ですが、「go func」と書いて、関数の処理をfuncの中に書いていきます。そうすると、中の部分がGoルーチンとして別に切り出されて実行される動きになります。
これは、実際に引数の配列を受け取っていて、その配列の要素単位にめちゃくちゃGoルーチンを起動する実行例です。自分の経験則では、1ミリセック以上のタスクであれば、逐次で起動しまくっても大きなオーバーヘッドにはならないと見ています。
そして、CPUを可能な限り使ってGoランタイムが実行をしてくて、ネットワーク通信などの処理も積極的にGoルーチン化することで、CPUが必要なGoルーチンを優先的に割り当ててくれます。
ではそのGoルーチンのもう1個の例として、ストリーミング処理にGoルーチンを使う例を話したいと思います。実際に(私たちのサービスで)やっているものですが、車両のGPS座標データをDBに取り込む処理があります。その処理を3つの段階に分けました。データを受信する処理、受信したデータをデコードして整形する処理、そしてDBに書き込む処理の3つです。こんな感じで、段階に分けて分割します。
そして、段階に分けて分割したものを、それぞれGoルーチンで実行します。この時に、真ん中のデコード・整形処理みたいなより高負荷な処理のところでGoルーチンをたくさん起動しておいて、そこだけ並列数を増やして処理をすることができます。
(スライドを示して)ではこのGoルーチン、それぞれ起動したところで、Goルーチン間のデータ渡しはどうすれば良いかというのを次に話します。
森下:ストリーミング処理のデータ渡しにチャネルを使います。チャネルはFIFOバッファになっています。Goルーチン間にチャネルを置いて、その間をチャネルを使ってデータをやり取りするように実装できます。その実装もすごくシンプルな記法でできて、「変数 <- チャネル名
」「チャネル名 <- 変数
」で受け取りと受け渡しができます。
(スライドを示して)右側のコードは実際にそういう処理をした例ですが、上部が実行というところで、forループで64並列でGoルーチンをめちゃくちゃgo funcで起動しまくっておいて、1個1個の処理はfor文で無限ループするようにしておきます。
その一つひとつの処理の中では、一つひとつのgo func自体が64並列に起動したGoルーチンに当たって、その中で前のチャネルから受け取って、処理をして、次のチャネルに渡すというような実装ができます。forループなのでずっとぐるぐる回ります。このチャネルですが、中に滞留できるデータの量を設定できて、0だと同期処理に、1以上だと非同期処理になります。
これだけだとイメージが湧きにくいので、全体の動きを次に説明したいと思います。
森下:(スライドを示して)チャネルの実行イメージに、先ほどのGoルーチンと間にチャネルがあるイメージを書いてみました。まずデータの受信側、次のGoルーチンにチャネル経由で渡す側としては、データを受信したらチャネルにデータを入れます。
そしてこのGoルーチンはデータを入れたらそれで終わりで、すぐに次のデータの処理に入ります。なのでここは非同期に、ひたすらチャネルの中にデータを送り続けることができます。
そして、真ん中のGoルーチンは複数のGoルーチンが立っているわけですが、それぞれのGoルーチンで、チャネルからデータを1個ずつ取り出します。この時、もしチャネルの中が空だったら、そのGoルーチンはチャネルの中に値が入るまで待機している動きになります。
そして処理が終わると後ろのチャネルBにデータを渡して、for文でまた次のチャネルからデータを取って処理するというかたちで、動き続けることができます。
というように、それぞれのGoルーチンがチャネルを経由して、それぞれチャネルにデータを入れる処理と同期せずに動かすことができます。そして、処理の負荷によってチャネルに入るこのデータの数や、それぞれ処理するGoルーチンの数を調整したりします。
森下:こういうのをやるわけですが、このGoルーチンとチャネルでよくトラブルがあります。よくこういうストリーミング処理を実装すると、「このぐらいで入ってくるはずなのに、実際書き込んでいる量が少ない」みたいな、「どこかで詰まっているんじゃないかな」という処理によく出会います。
実際に期待するスループットが出ていないことがよくあります。(スライドを示して)こういうものをどうするかというと、自分の場合にはこうやっています。各チャネルの中にデータが入っているわけですが、チャネルの中に入っている現在のデータの量を一定時間ごとにログ出力、だいたい1分ぐらいに1回ぐらいのペースでログ出力しておきます。
デコード・整形処理部分がめちゃくちゃ詰まっている時には、その前のチャネルにめちゃくちゃデータが滞留していて、たいていは内容量のマックスに近い量になっていて、その次のところでは容量が0に近いことになっています。なので、処理で詰まっていることがチャネルからわかります。
この対処例はどうするかというと、詰まっているGoルーチンの数をさらに増やしたり、中が通信処理の場合には、相手先マイクロサービスで詰まっていないかを確認したりします。
もしCPUが100パーセントだったらプロセスの限界なので、プロセスのスケールアウトをするか、スケールアップを検討することになります。つまり、Goルーチンの1つのプロセスの中の世界じゃなくて、それより前のところ、受信処理のところでスケールアウトするか、そもそもたくさんのCPUが使えるインスタンスに移すかということをやったりします。
実際にこうやってトラブルなどを解決して、Goルーチン、チャネルを使ってストリーミング処理をしています。
森下:以上、Goの並列処理について話しました。Goルーチン、チャネルという軽量スレッドと、軽量スレッド間通信バッファが構文レベルで提供されていて、非常にすっきり記述できて、良いものになっています。
経験上、Goルーチンは1ミリセック以上なら、1処理で逐次起動してもかまいません。事前に起動しておいて、仕事を割り当てることを実装しなくてもよいです。
ストリーミング処理は、Goルーチンを起動しておいて、チャネルを使って処理を部分的に並列に実行できます。さらに、チャネルの内容量をログ出力しておくと、ストリーミングで詰まっている処理がわかります。こういったかたちのGoルーチンとチャネルで、並列処理を実装しています。
私からは並列の基本のところと、Goの並列処理について話しました。
司会者:森下さん、ありがとうございました。質問がいくつかSlidoに来ていますね。ありがとうございます。なるほど。なにが一番答えやすいかな。けっこう長文もあります。上からいきますか?
森下:いいねがたくさんついていますね。「Goルーチンの実行が異なるコアに効率良く振り分けられないと、あまり良い並列性は得られない気がしたのですが、ランタイムにそれを伝えるオプションなどはあるのでしょうか?」。
本当に……ないんですよ(笑)。確かにそうなんですが、Go自体にそれはなくて、「すべてGoのランタイムに任せなさい」ということになっています。前職では、CPUが複数載っているマシンでCPUを100パーセント使いたい時には、どちらのCPUに割り当てるかみたいな制御したりしていました。
今時たくさんのCPUを使う処理においてスケールさせようと思うと、どちらかというと、プロセスの数をたくさん増やそうという概念になります。最近だとコンテナをたくさん増やして、たくさんプロセスが走るようにすることが多いので、1プロセスに対してCPUをどう割り振るかというのは、あまり問題にならないことのほうが多いかなと感じています。
せいぜい8CPUとか16CPUぐらいのものをたくさん立てましょうということが多いので、実際にはないのですが、今のところよほどチューニングが必要な環境でない限り、困ることはないというのが回答になると思います。
司会者:ありがとうございます。時間の都合もあるので、あと1個だけ回答するようにしますか? いかがでしょう?
森下:「Goルーチンがスレッド管理よりも軽量な理由が載った書籍などはありますか?」。実際にかつて自分が調べたことがあるのですが、書籍では出てはいないとは思います。
「チャネルで分割した場合、タスクのキャンセルや停止時にチャネルに溜まった処理を消費しきるなど実装の工夫が必要に見えますがどんな工夫をしていますか?」
そういったGraceful Shutdownみたいなのは、けっこう気を配らなきゃいけなくて、チャネルをクローズできて、クローズすると受け取り側はそのチャネルに新しく入れられませんが、0になるまで取り合いをし続けられるようになるので、その機能を使ってGraceful Shutdownを実装したりしています。
司会者:ありがとうございます。すべての質問に答えられずすみませんが、森下さんのパートを以上としたいと思います。ありがとうございます。
森下:ありがとうございました。
関連タグ:
2024.12.20
日本の約10倍がん患者が殺到し、病院はキャパオーバー ジャパンハートが描く医療の未来と、カンボジアに新病院を作る理由
2024.12.19
12万通りの「資格の組み合わせ」の中で厳選された60の項目 532の資格を持つ林雄次氏の新刊『資格のかけ算』の見所
2024.12.16
32歳で成績最下位から1年でトップ営業になれた理由 売るテクニックよりも大事な「あり方」
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
PR | 2024.12.20
モンスター化したExcelが、ある日突然崩壊 昭和のガス工事会社を生まれ変わらせた、起死回生のノーコード活用術
2024.12.12
会議で発言しやすくなる「心理的安全性」を高めるには ファシリテーションがうまい人の3つの条件
2024.12.18
「社長以外みんな儲かる給与設計」にした理由 経営者たちが語る、優秀な人材集め・会社を発展させるためのヒント
2024.12.17
面接で「後輩を指導できなさそう」と思われる人の伝え方 歳を重ねるほど重視される経験の「ノウハウ化」
2024.12.13
ファシリテーターは「しゃべらないほうがいい」理由 入山章栄氏が語る、心理的安全性の高い場を作るポイント
2024.12.10
メールのラリー回数でわかる「評価されない人」の特徴 職場での評価を下げる行動5選
Climbers Startup JAPAN EXPO 2024 - 秋 -
2024.11.20 - 2024.11.21
『主体的なキャリア形成』を考える~資格のかけ算について〜
2024.12.07 - 2024.12.07
Startup CTO of the year 2024
2024.11.19 - 2024.11.19
社員の力を引き出す経営戦略〜ひとり一人が自ら成長する組織づくり〜
2024.11.20 - 2024.11.20
「確率思考」で未来を見通す 事業を成功に導く意思決定 ~エビデンス・ベースド・マーケティング思考の調査分析で事業に有効な予測手法とは~
2024.11.05 - 2024.11.05