インターネット広告のプレイヤーたち

黒崎優太氏(以下、黒崎):では、「月間数千億リクエストをさばく技術」というタイトルで、発表させていただきたいと思います。よろしくお願いします。

僕はサイバーエージェントの黒崎と申します。サイバーエージェントは大きく分けて3つ、ゲームとメディアと広告という領域があるんですが、僕はそこの広告システムをつくるアドテクスタジオという部署にいます。今、入社4年目で、2015年に新卒で入りました。

今日ご紹介する「Dynalyst」というプロダクトのシステムとアプリケーション開発、それとインフラを担当していて、今日はそこの構成や、社内カンファレンスでお話したものをもとにご紹介しようと思ってます。

まず、Dynalystのお話の前に、インターネット広告のお話をしようと思います。この中で、普段広告系のシステムに業務で携られている方はどれぐらいいらっしゃいますか?

(会場挙手)

あ、意外といらっしゃいますね。そうするとそういう方には、よく知っているお話かもしれませんが、インターネット広告って、このようにたくさんのプレイヤーがいます。

サイバーエージェントはもちろん広告代理店でもあるんですが、アドテクスタジオは広告主の広告を出す、広告を出す側のDSPであったり、メディアサイド、パブリッシャー側なんですけど、SSPと言って各広告枠を束ねていくような存在であったりとか。

あと、アドネットワークという、広告主も枠も持っているみたいな存在であったり、それらに対してユーザーの属性などの情報を出すDMPという存在など。あとは、それらの広告効果を測定するための測定ツールなど、ひととおり存在しているという感じですね。

弊社ではいろいろなプロダクトがあるんですが、だいたいこういう感じで、DynalystというのはここのDSPというところにいます。

とくにこのSSPとDSPの間は、広告の買い付けのやりとりがオークションでリアルタイムにやるんですが、その仕組みを軽くお話したいと思います。

RTBの仕組み

RTB、Real Time Biddingと言うんですが、僕らみたいなDSP事業者がたくさんいまして、SSPと接続しています。例えばあるWebサイトを見た時に、ここが広告枠だとします。

そうすると、Webページがロードされたタイミングで広告タグが発火して、SSPに通知が行きます。すると、ここを見にきたユーザーのCookieや端末、広告用の端末IDなど、そういったものをもとにbid requestというリクエストが飛んでいきます。

そして、各DSPはその情報をもとに、広告枠の情報やユーザーのIDなど、そこに対して広告を出すのか出さないのかというところと、出すとしたら何を出すのか、あとはいくらで入札するのかというを、内部で計算します。

例えば500円、300円、700円みたいな、各自入札枠を決めたとしたら、それをいくらで買うかというのと、勝った時にどんな広告を出すかという、HTMLなど、そういう内容をレスポンスします。この中だと一番高いのは700円で入札したDSPなので、ここのDSPが勝ちます。

勝ったら、その広告主の広告が広告としてユーザーにロードされます。多くはセカンドプライス・オークションといって、一番高い人が勝って、その時に勝った価格は2番目の値段なので、ここのDSPは700円で入札して、セカンドプライスの500円でここの枠を買ったということになります。

これを高速に、みなさんがWebサイトを見られている間の広告枠は、これを高速に繰り返すことで、枠が取引されています。だいたいなんですけど、だいたい100ミリ秒以内に取引が終わらないといけない、というルールが多いです。100ミリ秒って0.1秒なんですけど、それ以上遅くなると広告の表示が遅くなってしまうので、できるだけここは高速にやる必要があります。

DSPをご紹介したんですが、僕らはスマホゲーム向けの広告配信プラットフォームをやっていまして、国内のスマホゲームだとわりと高いシェアを持っています。海外だと日本・アメリカを含んで、7ヶ国ほどに配信していて、ゲームをプレイしているユーザーに最適化した広告を配信しています。

もちろん、お客さまはたくさんいらっしゃるんですが、Dynalystの公式サイトで公表してるかぎりだと、こういったお客さま方がいます。最近は動画広告も配信してまして、このクリエイティブも、弊社のクリエイティブチームが制作しています。

月間数千億リクエストをさばく技術

では、月間数千億リクエストをさばく技術、本題のところに入って行きたいと思います。DynalystはほぼすべてAWSを利用しています。だいたいコンソールの画面を見ながら、使っていそうなものをピックアップしました。有名どころはだいたい使っています。

他であまり使われてなさそうなところで言うと、Data PipelineやMediaConvertなど。あと、SESはメールなんですが、一部お客さまと連携するのにメールで送信されてくるデータを自動で取り込むためにSESを使ったりしてます。

ざっくり言うと、弊社のシステムは東京リージョンと、us-east-1なのでバージニアのリージョンに、2リージョンで展開しています。もともと、当初Dynalystをつくり始めたのは、今から4年ちょっと前のことです。その時に最初から海外でも勝負がしたいという要望がありました。。

当時、弊社ではオンプレを使うか、もしくはクラウドと組み合わせることが多かったんですが、うちのチームは当初から海外でやりたいというところを目指していたので、がっつりパブリッククラウドに乗るという選択をしました。現在は、アプリケーションのコンポーネント的には100種類ちょっとほどのアプリケーションがいろいろ組み合わさって構成されています。

日本とアメリカと2リージョンなんですが、どんな構成になってるかというと、もともとは日本で始めたので、だいたいのものはすべて日本にあります。入札や配信、計測、管理画面、バッチ処理など、集計も日本にあります。

アメリカには入札・配信系と、それらに必要なデータを取り込んだりするためのバッチ系だけしかなくて、基本はS3にもログが上がってくるんですが、cross region replicationして、すべて日本に送っています。そして、集計は日本でやってる、というかたちになります。

どれぐらいリクエストが来ているかというと、「月間数千億リクエスト」という言い方をしているんですが……。これは日本の入札リクエスト、具体的な数字は言えないので、だいたい秒間数十万リクエストという感じです。

日本のグラフの左端の山は朝ですね。これは平日なんですが、みなさんが起きられて、おそらくここが通勤ラッシュで、12時はお昼休み。そして仕事に戻って、だんだん夜にかけてピークが来て、12時ぐらいになって寝てトラフィックが落ちる、という感じです。

アメリカもだいたいそういう感じなんですが、日本とだいぶ形が違うのは、僕の中では印象的でした。あまり、お昼休みでとがるということはないんです。ただ、日本とアメリカで概ね同じぐらいのリクエストを受けているので、トラフィック量としてはだいたい同じぐらいです。

あとは、入札しているサーバーのレスポンスタイムで言うと、上の青いほうが日本で、下がアメリカなんですけど、だいたい僕らがレスポンスタイムと言ってるのは、ロードバランサーとサーバーの間の時間なんですが、だいたい僕らは20ms以内に抑えるようにしています。そうすると残り80msあるので、オークションがタイムアウトすることはないだろう、という感じです。

そして、これは最近のある日のトラフィックです。これも外にはあまり出せないものなんですが、だいたいこんな感じです。

そして、ある日だけ、夜にこうなりました。いつもならトラフィックが増加するところで凹んでいるのがわかりますね。みなさんわかるかもしれませんが、これがこのあいだサッカーのワールドカップをやっていた時ですね。

ハーフタイムのところできれいにトラフィックがガーッと戻ります。たぶんここの上下した分が、普段はスマホいじっていたであろう人たちがスマホをいじらなくなって、テレビに集中していた人のリクエスト分がだいたいこの振れ幅分ぐらいという。僕らから見てもグラフではっきりわかるかたちで影響が出ていたので、かなり大きいイベントだったんだなあ、というのが分かりました。

あとは、試合が終わった後、おそらくみなさんニュースの速報記事などを読まれていると思うので、それでかなりインプレッションが増えて、普段よりだいぶガッと。普段ならみなさん寝ているような時間なんですが、ガッと盛り上がって、「寝るか」ということでここからはだいたい一緒みたいな感じですね。

こちらは広告のトラフィックではなくて、インターネットエクスチェンジのあるところのトラフィックなんですが、やはり同じようにへこんでいます。僕らの広告のトラフィックは、だいたいインターネットのトラフィックと同じような形をしています。

そしてこの「bidリクエスト」というのは入札のリクエストなんですが、これを全部足し上げると、だいたい1ヶ月で数千億リクエストぐらいを毎月受けており、秒間数十万リクエストという規模になっています。

こうした大量のリクエストを受けるためにどうするかというと、とくにめずらしい構成をしているわけではないんですが、Dynalystだとアベイラビリティゾーンを2つ使っていて、ロードバランサーがそれぞれあって、EC2インスタンスに入札のサーバーを大量に展開しています。

そして、RDBではAmazonのAuroraを使っています。あとはユーザーの属性のデータをDynamoDBに入れていて、ElastiCacheが前段にいてという感じです。1個のサーバーがc4.4xlargeを使っていて、16コア、30ギガぐらいです。ピークだと日本とアメリカ合わせて、だいたい100台ちょっとぐらい動いているので、リソースとしてはそれなりに大きいかなと思っています。でも、これでもだいたいピークの時は、CPUをそれなりに使い切るぐらい回っています。

あと、最近、c5インスタンスも日本で入れ始めました。性能的には僕らのサーバーの秒間あたりのパフォーマンスはだいたい1.1倍ぐらい上がりました。確か値段もc4より安かったと思うので、だいぶお得なインスタンスだなと思ってます。

ただ、日本だとまだ在庫が足りない時があるので、c4と組み合わせないと、スケールアウトしたい時に在庫がなくて、リソース不足になって、レイテンシが跳ね上がってしまうという事態になるので、そこはまだ様子見しているという感じです。できれば全部c5に変えたい、という気持ちがあります。

Scalaを使う理由

僕らはアプリケーションを、ほぼすべてScalaという言語で書いています。どうしてScalaという言語を採用したのかというのは、記事があるんですけども。4、5年前ですね、アドテクスタジオができた時に、「Scalaという言語をメインに使っていって、いろんなプロダクトを立ち上げよう」と言った方がいました。

その時の理由としては、技術的なチャレンジであったり、Javaの資産が使えるうえにハイパフォーマンスでメンテナンス性が高いなど、いろいろあったんですけども。

最近だとこの言語を使ってるところもかなり増えてきましたが、アドテクスタジオではある種、当たり前のようにScalaが使われているので、今となってはチャレンジというか、けっこう普通に使われているので、逆に知見がすごいたまっていて、開発速度の向上にも貢献しているんじゃないかなと思います。

あとは、関数型言語方面に寄せようと思えば、そういう書き方もできたりして、関数型の勉強会も社内でやっていたりします。

あとは、Scalaで僕がすごく気に入っているところなんですが、Futureというものがありまして、何ができるかというと、例えばこれ、データベースにクエリを投げるのをブロッキングするような処理を、Futureで括ると、スレッドプールからスレッドがディスパッチされて、別のスレッドでその分のコードが走ります。

だから、ここのメソッドを呼び出した側からすると、Futureをブロッキングするものを全部Futureで括っておけば、基本的にあまり深く考えなくても並列処理ができて、処理時間がかなり効率化して、CPUを効率良く使えます。

例えばどんな時に使っているかというと、これは適当に書いたコードなので、僕らのところで動いているコードではないんですが、例えば、AuroraやMySQLのサーバーに2回クエリを投げて、さらにその結果をもとにDynamoDBにクエリをしなきゃいけないというユースケースがあったとします。

直列にこれ引いて聞いて、これ引いて聞いて、これ引いて聞いてとやると、普通に時間3つ分が足し合わさるので、4ms、4ms、8msかかるとしたら、16msかかるかもしれません。

これらがデータのソースとして依存関係がなく独立していれば、Futureで返ってくるもの3つ同時に呼び出せば、3つのスレッドで同時にクエリがされて、それが同時に返ってくるので、一番遅い8ミリ秒で処理ができて、全体としては半分の時間でレスポンスできる、という構造になっています。

ログはすべてKinesisに流す

こうしたことを組み合わせて僕らはScalaで書いてるんですが、ログの処理のところで言うと、基本サーバーが吐いたログは全部Kinesisのストリームに投げています。

なぜそうしているかというと、当初Kinesisを使う前は、直接サーバーからS3にログをアップロードしたりしていたんですが、結局ローカルでファイルでデータをバッファリングするということは、そのサーバーが死んだ時にその分のログが欠損したりします。

あとは、トラフィックが増えてくると、大量にためて一気にアップロードしたりするので、その瞬間だけネットワークの帯域を使って不安定になったりと、いろいろデメリットがあります。

あとは、サーバーのアプリケーションで、ログの加工をしてアーカイブしやすいようなものにするところまで書かなければいけないなど、いろいろと面倒くさいので、基本的にログを作ったらすぐKinesisにアップロードしてしまっています。

そのストリームに対するコンシューマーがいろいろいて、例えば流れてきたログをもとに、データベースをアップデートするようなコンシューマーがいたり、あとはそのログをそのままS3にアーカイブするようなコンシューマーだったり、用途ごとにコンシューマーを足せば、こちら側はなにもしなくても、いろいろな用途に使えるようになるので、僕らはこうした構成にしています。

あとは、このあたりががもし障害になったとしても、こちらの配信には影響しないので、慌てることもなく、ここがストリームなので、コンシュームしなければその分をずっと保存しておいてくれるので、障害への耐性も強いつくりになっています。

投げる時は、僕らはfluentdを間に入れています。

そして、Kinesis Producer Libraryというものがありまして、これはAWSが出しているライブラリなんですが、どういうものかというと、Kinesisというシャードがあるんですが、1シャードあたり1秒間に1,000レコード、……「しか」と言うと語弊があるかもしれないんですが、1秒間に1,000レコード or 1Mbpsまでという制約がありまして、これを1個1個の小さいログを1つのKinesisのレコードに入れて転送するのは、かなりもったいないんですね。

例えば1秒間に1メガバイトも送らないのに、小さいレコードが1,000レコード以上出てしまうと、イメージ的にはスカスカのログをたくさん送ってしまいます。本当はもっと詰め込めればいいのに、というのがあるんですが。Kinesis Producer Libraryを使うと、そこをよしなに埋まるように、レコードを1つのレコードに、1つのKinesisレコードに複数のログレコードを突っ込んで、変換して投げてくれます。

もう1個、別側で投げられた、詰め込まれたログをコンシュームするんですが、それをよしなに展開する側のライブラリもあります。さきほどは“Producer”だったんですが、こちらはKinesis Cosumer Libraryというもので、集約したレコードを展開して、アプリケーションの中で使えるようになっています。

あと、一番大きいのは、このようにコンシューマーがたくさんいて、シャードがたくさんあるんですが、どのコンシューマーがどのシャードを担当するかという調停をお互いにやる機能もそのライブラリに内蔵されていて、僕らがアプリケーションの実装でコンシューマーが増えた時に、「このシャードはこれが担当だ」みたいなことはやる必要がありません。裏側で調停するためのデータベースとして、DynamoDBが使われています。

例えばコンシューマーが障害が起きてしまって、そうするとここのストリームを見にいくコンシューマーがいなくなってしまうので、このシャードには消化に遅延が発生してしまうんですが、自動で再割り当てがされて、こいつがこっちに見にいくみたいなことは自動でやってくれます。

ユーザーデータの扱いについて

DynamoDB。僕らが一番お世話になっているのは、ユーザーのデータを格納する用途としてです。どのように使っているかというと、IDFAやAdvertisingIDというスマホ広告用の端末IDなんですが、例えばあるゲームのお客さんだったとして、最後にログインした時刻がここに入っているとします。

そして、広告を入札する時に、例えば「ログインしなくなってから3日以上経った人に対して広告を配信したい」みたいな条件を設定します。それがたくさんのいろいろなタイトルやゲーム、アプリなどいろいろあるんですが、それらに対して候補を、広告を入札する時に、1回で配信の候補を出さないといけないんですね。

そのために、僕らはどうしてるかというと、Hash KeyとRange Keyを組み合わせています。

例えば、Hash KeyはDEVICE_IDと同じなんです。ですが、もう1個Range Keyというものがあって、ここに広告主のIDを入れています。

こうすると、例えばDEVICE_001という人の広告の入札リクエストが来た時に、その人に対して配信する広告の候補を1クエリで出したいんですが、この時はHash Keyだけで引けば、複数のレコード返ってきます。なので、例えばここの人だったら、この2レコードが返ってきて、それを配信候補として計算することができます。

ホットパーティション問題をどうするか?

あとはDynamoDBはAWSのマネージド・サービスのキーバリューストアなので、お金を払えば上に無限にスケールするかというと1つ問題があります。デバイスIDは乱数のようなもので生成されてるいるので、基本的にはキーが偏ることはないと思われがちなんですが、ホットパーティション問題というものがあります。

結局、登録されたレコードが入った時が新しいものなのか古いものなのか、期間がかなり長いものが混ざっていると、どうしても偏りやすいという問題があります。

なぜかというと、新しく自分たちのデータベースに入ってきたレコードって、そのユーザーがアクティブなので参照されやすいんですが、ある程度期間が経ったものって、例えば端末ID、広告用なのでリセットしたり、オプトアウトしたり、そもそも端末が古くなってその端末使われなくなったりなど、そうした要因があるので、新しいデータと古いデータ、これがパーティションだとすると、混ざっているとアクセス量が偏ってしまいます。

そうすると、Readのキャパシティとかをプロビジョンしても、全体としてはプロビジョンした容量よりも半分以下しか使っていないのに、すぐスロットリングが発生してしまったりなど、そういう問題が起きてしまいます。ですので、登録されてからの期間があまりにも違うようなデータは、1つのテーブルに混ぜないようにすることが必要です。

キャッシュは、memcached入れてます。最近はr4インスタンスにしたんですが、これは何がいいかというと、確かr3までは1インスタンスあたり2Gbpsぐらいまで。まあ、インスタンスサイドにもよりますが、やはりスパイクした時に帯域が足らなくなってしまって、そこでレイテンシが跳ねるみたいなことがありました。

そもそもr4インスタンスは、ネットワーク帯域が一時的に10Gbpsまでバーストできるので、一時的にユーザーのデータの入れ替わりがあったりして、キャッシュが大量に切れるような事態が発生しても、多少の時間であればバーストができるので、これで帯域が頭打ちになってしまうという事態が緩和できています。

DynamoDBのキャッシュについて

DynamoDBではDynamoDB Acceleratorというのがあるんですが、それは僕らは使っていません。なぜかというと、僕らはDynamoDBにデータを取りに行く時に、全アトリビュートを取ってくるのではなく、必要なアトリビュートをクエリで取ってきたリしています。

DynamoDB Acceleratorって、普通に全レコードセットして全レコードゲットするみたいな用途だと、ちゃんと自動でキャッシュのインバリデーションをしてくれるんですが、クエリで更新したものに対してはそれができないので、そうするとデータベースが更新されても古いデータを見てしまったりということもあるので、僕らはまだちょっとそこのデータをキャッシュから更新がかかった時に消すのは、自分たちで実装をしています。

どんな仕組みかというと、ユーザーのデータをアップデートしたらDynamoDBに書き込むんですが、DynamoDBに書き込んだイベントがDynamoDB Streamsに流れるんですが、そのストリームのコンシューマーとしてLambda Functionを使っています。

あるユーザーAというレコードがアップデートされたら、そこのイベントがストリームに流れるので、Lambdaがmemcachedに対して、そこのユーザーのキーのレコードを消して、TTLが切れるまで待つのではなく、こちらから消しにいっています。

そうすると、次に広告を入札する時に聞きにいった時はキャッシュがなくて、データベースに聞きにいくので、できるだけリアルタイムの情報が広告の入札に反映できて、ユーザーの状態に応じた広告が出しやすい、ということをやっています。

あとはログの集計なんですが、だいたい入札のログだけで、1日3TBぐらいと書いています。たぶん圧縮した状態だった気がするんですが、だいたいこのぐらいの量がたまってしまっていて、そうすると適当なデータベースに突っ込んで集計するということが難しいです。

あと、入札だけでなく、実際に広告が表示されたログをはじめ、クリックされたりクリックされた後にログインしたとか、そうしたログと紐付けして広告のレポーティングをしないといけないので、Apache Sparkを使っています。これもScalaで書けるんですが、AmazonのEMRというクラスタの上でSparkを動かしています。

具体的にどう書くかというと、SparkでもSpark SQLというものがあってSQLで書くこともできます。Scalaのコードで処理を記述することもできて、例えばこういうSQLがあるとしたら、Scalaだと、そのレコードをcase classに定義して、だいたい同じような、GROUP BYの部分はgroupByというメソッド呼んで、そのキーにするものをコードで抽出しています。

どうしてDynalystではSQLではなくこちらのコードで書いてるかというと、例えばここのログのタイムフォーマットのパースをしたり、広告主によってタイムゾーンが違ったりするので、そのタイムゾーンを広告主IDによって、データベースからタイムゾーンを引いてきて、タイムゾーン変換をかけてから集計したりとか。あとは、この広告主はこの計測ツールからこういうデータを紐付けてたり。

まあ、いろいろ集計するために必要な処理があり、そこをSQLで書くとテストもしづらくて大変なので、Scalaで書いています。そして、Sparkのフレームワークに則って書くと、ここで書いたものが各ノードにスケジューリングされて分散されて実行される、という仕組みになっています。

なぜ信頼性が求められるのか?

かなりパフォーマンスを要求されるシステムだと思うんですが、どうして信頼性が求められるのか。結果的になぜ必要なのかということと同じような理由になってきてしまうんですけども。

みなさんが想像しやすいメディア系のサービスだと、サービスが提供できなくなったら、ユーザーが離脱してしまって、そこで収益が低下したり、信用を失うみたいなことがあると思うんですが、広告配信の場合は、正直広告が出なくなっても、そのメディアを使っているユーザーになにか影響があるかというと、ユーザーに対して直接影響が出るわけではなく、広告主に対して影響が出ます。

広告が配信できないので、その分機会損失が発生してしまって、僕らの売上も下がりますし、広告主も広告効果が下がってしまって、売上が下がってしまうとか。あとは、あまりにも不安定なシステムは使いたくないと思うので、広告主がそもそも離脱してしまって、僕らの収益が下がったりということもあるので、できるだけ落とさないことが求められます。

僕らのシステムもだいぶお客さんが増えてきて規模も大きくなってきたので、規模が10倍になると、ある時間に止まってしまった時の損失も10倍になってしまいます。

今は1時間止まってしますと、数字としてはかなり大きいなインパクトが出てしまうので、できるだけ落ちなかったり、障害が起きても配信には影響させずに、あとでどうにかリカバリーできるシステムにすることを設計する時は最優先に考えています。

どうすればそうできるのかというと、基本的に僕らはパブリッククラウドを使っているので、できるだけパブリッククラウドが提供しているものにきれいに乗るようなシステムを考えています。

そうじゃないと、結局、自分たちでマネージドするものが増えてしまいます。チームに専業のインフラエンジニアがいなくて、パブリッククラウドを使っている分、勉強すれば誰もが平等に使えるような環境を作っています。

「このシステムの運用はこの人に任せる」ということはやっていないので、システムの面倒を見る部分などは、マネージドサービスで自動でスケーリングするなりリカバリするというところに寄せて、僕らはアプリケーションコードを書くところに集中できるようにする、という考え方でシステムを構築しています。

もちろんAWSのソリューションアーキテクトに相談して、「ここはもっとこうしたほうがいいんじゃないですか」というアドバイスはもらったりしています。あとは、自動化できるのもいいんですが、まだすこし不安なところはあります。完全にシステムの運用を自動化できるものがあったとしても、やっぱり手動でなにか緊急の対応ができたり、できるような手段をまだ用意するようにしています。

ここはもっとクラウドに乗った運用を自動化をするようなものがもっと乗せられれば、手動でどうこうするというところすら考えなくていいようにできるかなと思ってます。正直僕らは、まだそこまではいってないという認識です。

監視とパフォーマンス調査

監視はDatadogを使っています。さきほど見せたようなダッシュボードがあったり、もちろんアラートもこれで監視しています。

あと、広告主ごとにどのぐらい広告を配信してるのかみたいなのも、Datadogで見ることができます。だから、システムのメトリクスだけでなく、お客さんのメトリクスもこうやって取っています。細かくて見づらいかもしれないんですが、この1個の色が1つの広告主と思っていただければ問題ありません。これでだいたい、突然どこかの広告主のボリュームが増えたとかしぼんだというのもわかるようにしています。

あとは、リクエストがすごく急に来て詰まってしまった時には、これはオークションできるんですが、HTTPのステータスコードの204である、「No Content」を返すと「入札しない」という意思表示になります。例えばアプリケーションの処理時間中に入札の計算が終わらなかった場合、そこで処理を打ち切って、「入札しない」と返してリクエストが詰まらないようにしています。

今後は、キャッシュ層やDB層をオートスケーリングできるようにする、というチャレンジをやっていきたいなと思っています。あとは、E2Eテストというのは、実際にリリースされる環境において、自動でbotが巡回して、広告枠に接触して、広告が入札されて、ということが常に自動で巡回してテストされるようなものをつくりたいと思って考えているところです。

こんな感じになります。ありがとうございました。

(会場拍手)