LINE Developer Day 2019 Day1 レポーティング(と言う名の自分的メモ)
gRPCの事例について聞きたいだけの人生だった
Motivation
参加のMotivationは上の見出しの通りです。 gRPCで、超高トラフィック、BtoC、これほど事例として、パーフェクトなものはなかなかお目に書かれるものではありません。
結果、それ以外にも非常に勉強になる情報を持ち帰って来れたので、非常に有意義な時間でした。
LINE Developer Day Is 何?
LINEさんの提供するサービスに関する技術スタックに関するチャレンジによる知見の共有、そして、今後LINEさんが提供するサービスのロードマップを紹介する技術カンファレンスです。
今回は初めて2日体制で行われたようで、
Day1=>Engineering
Day2=>Production
がテーマでした。
KeyNote
LINE Payのユーザー数:日本=>3700万 ワールドワイド=> 5000万 LINE Payに関わらず、近年力を入れ積極的にAIの力を利用している。(AIカンパニーとしてのブランディングを感じました。)
LINE BRAIN
LINE BRAIN室 室長 いさごさん(元々Microsoftにいらっしゃった方だったので、この業界の人材流動性の高さを改めて感じてみる。)
AI OCRの話
機械学習を使ってのフォント作成チャレンジ事例の紹介。
フォント作成のためには、通常数千文字の手書き文字が必要。 早稲田大学2年生の田村くんが手書き風レポートマシンを作ったときは7000字以上の文字データを入力したらしい。 www.mbs.jp LINEが開発したAI OCRの方は、500字程度でフォント作成が可能。
※ この件、個人的には、早稲田生の田村くんに、大きな拍手を送りたいお気持ち。実行力、実現力もそうですが、マインドセットが素晴らしい。
LINE Ai Callの話
自然言語処理技術の事例として紹介されました。 電話での予約受付と簡単な問い合わせ対応を全てAIが対応してくれるらしい。コミュ障にやさしい。
[俺のGrill&Bakery 大手町] に実験導入中。固定電話から電話を掛けると、AIが対応してくれる。(なるほど、こういうのを指して、人間の仕事を奪うって言う表現がまかり通るのか。というお気持ち。) tabelog.com
KeyNoteの印象
これはKeyNoteに関わらず感じたことでしたが、LINEさんはミッションドリブンな企業風土だなーという印象でした。 プレイヤー / マネージャーのエンゲージメントも高いように(外からは)感じられましたし、ミッション遂行のために技術を磨いて当たり前という空気感は非常に気持ちが良かったです。
プライベート Kubernetes クラスタにおける gRPC サービス開発
LINE LIVE サーバーサイドエンジニア宇井さんのセッション。 LINE LIVEの、Live Commerce(アーティストの応援がてら購入しちゃう機能っぽい)機能の構築に、k8s, gRPCでの構築を行ったお話。
gRPCの選定理由は様々
個人的に気になっていたgPRCの選定理由。聞いてきた内容は以下(実際はとても丁寧にご対応頂きましたが量が多いので文章雑にお送りします。)
MicroServiceだとREST/Json は遅い。とにかく遅い。
バイナリーでやり取りしたい。速くしたい。
あと、今回は新機能開発でチャレンジできるので、社内に知見が少なかったgRPCを選択した。
チャット機能も選定当初は実装する予定だったので、双方向通信でもgRPCは利用する予定だった。(工数の都合上、最終的にチャット機能は本体側のもの?を利用することになった。)
ClientサイドでJsonほぐすのめっちゃだるいし、Protobuf最高な人生だった。(クライアント担当の方の意見)
サーバーサイドでも。Protobufだと更新楽だから生産性が向上する。(この辺はRESTでもSwaggerからプロキシクラスを生成出来るけど、クライアントをFlutter使っていることもあって、gRPC楽って感覚が強かったような感触。)
登壇されていた宇井さんも淡々と大量に情報をくださる方で、感謝しかないです。m(__)m
また、ブースに行ってご質問をさせていただいた相手の方が、クライアントの開発者の方だったのですが、サーバーサイドに関する質問にも丁寧にご回答頂き、非常に助かりましたし、クライアントの方でもここまで理解し、説明できることに感動しました。
また、意外というか、gRPC For Webを既にProdで使っているという事例は、非常に勇気を貰いました。
これは自分の先入観だと思いますが、サーバー間通信はgRPCがベターと思っていましたが、Webブラウザー等のClientからのリクエストはまだまだProdはRESTかなと思っていたので、この話を伺うことが出来たのは非常に心強かったです。
Inside Of Blog; 15年間熟成されたサービスの光と影、カオスとレガシーの挑戦
LINE Blog,Livedoor BlogのServer Migrationを担当している大森さんのセッション。
livedoorブログのダークサイド
ドキュメントがない。
開発サーバーもない
livedoorブログをLineブログに統合させるために、まず開発サーバーを作る。
テストコードもない
DNSのレコードが多すぎた(over 300 Records) ※ なお、230は不要なレコードだった。
言語バージョンを、17年間固定してた。(perl 5.8)→逆に技術高い
少しだけ新しいバージョンのperl(5.16)も混ぜてモジュールメンテナンスしてるので、管理辛い
MySQLが4.0
lineの社内共通基盤にはMySQLがそもそも乗らない
永続化層の基盤には乗せられないのでAPサーバーの基盤にMySQLを乗せて解決(?)した。
MySQL Client 5.7.5からはMySQL4.0に接続できなくなるから、どうしよう?←イマココ
livedoorブログをline側の基盤に移行させるプロジェクト、2017年から始まっていて、2020年の前半までやるらしい。
なお、LINEブログはLivedoorブログをForkして作ってるけど、こっちはエンジニアが本気出したのでなんとかしました。
LINEブログだけを直したのは、経営判断でしょうけど、このへん開発者は、livedoorブログの方も直したくなっちゃうから気を付けないと行けないなーというお気持ち。
この辺、きっちり選択と集中を実践し続けられているのは、素敵なことですね。
最後に仰っていた、「普段から少しでも負債と向き合って、ちっさいところでもいいから改善を入れていくことが大事」という言葉は、アプリケーションの成長っていう事柄に向き合う上で非常に大事。
負債と向き合わない理由を探す人が1人でもいると、変にパワーが取られて邪魔になるので、1人1人がちっさい改善でもいいからプロダクトの品質にコミットしていくっていうのは、自分含めて本当に多くの人に意識して欲しい。
ASP.NET CoreのStartup.csへのインジェクション以外で、appsettings.XXX.jsonファイルにアクセスする。
appsettings.XXX.json(要するにconfigの類)をProgram.csで参照したい
普段ASP.NET Core MVCで開発している場合(ここでいう通常、は)、Startupクラスにはappsettings.jsonとappsettings.{EnvironmentName}.jsonを読み込んだコンフィグがインジェクション 設定関連の情報については通常、ServiceDescriptorのConfigureでDI設定をすることが多いと思います。
こんな感じ
services.Configure<KashilogdatabaseConfig>(configuration.GetSection(nameof(KashilogdatabaseConfig)));
が、例によって、Program.csの中でconfigの類を読みたい欲があります。
書いてみた(今回はローカルメソッド切って書いたので、抜粋)
static IConfigurationRoot CreateDefaultConfigurationRoot(ConfigurationBuilder configurationBuilder) { return configurationBuilder .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{DefaultEnvironment.GetEnvironmentName()}.json") .Build(); }
DefaultEnvironment.GetEnvironmentName()は前回の記事で書いたので説明は省きます。 ConfigurationBuilderクラスに対して、[.SetBasePath]拡張メソッドと、[.AddJsonFile]拡張メソッドでappsettings.json / appsettings.{EnvironmentName}.jsonを追加して、Buildする簡単なお仕事です。 もちろんDefaultBuilderと違って、appsettings.jsonっていうファイル名じゃなくても大丈夫です。(DefaultBuilderでは、appsettings.json / appsettings.{EnvironmentName}.jsonを勝手に読むようになってます。) と言いつつ、appsettings.jsonと、各環境用のappsettings.{EnvironmentName}.jsonを読むことが多い気がするので、今回はこの2ファイルの追加のためのコードでお送りしました。
EnvironmentNameをIWebHostEnvironment.EnvironmentNameを使わずに取得してみる
EnvironmentNameをProgram.cs内で参照したい
具体的には、Mainメソッド内で
CreateHostBuilder(args).Build().Run();
ってやってるところで、ロギングしたかったり、CreateHostBuilderのところで、そもそもEnvironmentNameに対応したappsettings.XXX.jsonで、ロギングの設定を切り替えたい欲があります。(ProdではFatalだけでいいですし、開発環境ならDebugログまで全部ほしかったりするので。)
書いてみた
using System; namespace Microsoft.AspNetCore.Hosting { public static class DefaultEnvironmentNames { public const string Development = nameof(Development); public const string DevelopmentRemote = nameof(DevelopmentRemote); public const string Staging = nameof(Staging); public const string Production = nameof(Production); } public static class DefaultEnvironment { public static string GetEnvironmentName() { return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? DefaultEnvironmentNames.Production; } } }
DefaultEnvironment .GetEnvironmentName() という、環境変数"ASPNETCORE_ENVIRONMENT"を取得し、取得結果がnullであれば、"Production"を返却するだけのきわめてシンプルなメソッド(とそれを実現するためのクラス2つ)を書いてみました。 C#、というか.NETには、古の昔から環境変数を取り扱えるEnvironmentクラス(@System名前空間)が存在しているので、環境変数を取得するコード自体は随分昔から変わらないですね。 蓋を開けてみれば、.NET Framework時代に使っていた名前空間 / Object / Api の利用経験を思い出すことで解決可能でした。
C# / .NETは、バージョンが上がるたびに別物感を感じる瞬間があるんですが、やはり地続きなんだなあと感じました。(小並感)
ASP.NET CoreでProgram.csを見たり、Serverヘッダー:Kestrelをオフにしてみる
Program.cs周りのコードを確認してみる。
ASP.NET Core の2.2時代のテンプレートは、WebHost(IWebHostBuilder)が使用されていました。
コードベースはこんな感じ。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>();
3.0からはHost(Webに限定しない汎用的なホスト)を利用する形に変わりました。
コードベースはこんな感じ。
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
コードを見るとわかりますが、HostはWebHostも包含しているので、WebHostの設定は、ConfigureWebHostDefaultsの中に書いていきます。
Serverヘッダーをオフにしてみる
=>レガシーのASP.NET / MVC /WebApi時代も、x-powered-byとか、Serverとかのヘッダーが出てると色々アレなので消すことが多かったと思います。 Kestrelもつまるところ、ASP.NET Coreですよーっていう名札になるので同様に消したい欲があるので2.2 / 3.0でのServerヘッダーの消し方を見ていきましょう。
(ご参考)Serverヘッダーをオンにしたままだとこちら
.NET Core 2.2での書き方
2.2では.UseKestrelを使用します。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel(options => options.AddServerHeader = false) .UseStartup<Startup>();
.NET Core 3.0での書き方
3.0ではオプションを差し込むだけなら、ConfigureKestrelを使用します。 今回は本筋じゃないので割愛しますが、TestHostを利用する際などに、HostBuilderを生成する場合はUseKestrelを使用した後にConfigureKestrelを使用するようです。 このへんはTestHostの.NET Core 3.0対応をするタイミングで(たぶん)詳しく触れることになりそうです。
public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder .ConfigureKestrel(options => options.AddServerHeader = false) .UseStartup<Startup>(); }); }
Serverヘッダーをオフにした場合
.NET Core 2.2 => 3.0は色々使い勝手やパフォーマンスに配慮されて進化しているので、色々APIも統廃合や新設が進んでいますが、プロダクトをグロースさせるためにも引き続き学んでいこうと思います。(小並感)
C#8 GA後のswitch式を見てみる
GAのタイミングで再確認してみるswitch式
後置き構文で読みやすいswitch式ですが、プレビュー時代は、以下の書き方をする必要がありました。
static IEnvironmentBuilder GetEnvironmentBuilder(string environmentName) { return environmentName switch { DefaultEnvironmentNames.Development => (IEnvironmentBuilder)new DevelopmentBuilder(), DefaultEnvironmentNames.DevelopmentRemote => new DevelopmentRemoteBuilder(), DefaultEnvironmentNames.Staging => new StagingBuilder(), DefaultEnvironmentNames.Production => new ProductionBuilder(), _ => throw new System.ArgumentException($"{nameof(environmentName)}") }; }
なぜか推論出来ない問題(@プレビュー時代)
return してるので普通に考えると、IEnvironmentBuilder を返却する式であることは推論できるはずなんですが、1番目で、 (IEnvironmentBuilder)とキャストして上げないと、コード解析で赤波線が出ていました。(逆に最初に書きさえすれば、2番目以降はIEnvironmentBuilder を実装したオブジェクトを設定してあげていれば大丈夫でした。)
なんか不格好だなーと思っていたんですが、.NET Core 3対応を行っていくタイミングで、このキャスト部分がコード解析で、「冗長だぞ!」って言われるようになっていることに気づきました。
return してるときは、戻り値から推論してくれるように改善されているようです。なので今は、
static IEnvironmentBuilder GetEnvironmentBuilder(string environmentName) { return environmentName switch { DefaultEnvironmentNames.Development => new DevelopmentBuilder(), DefaultEnvironmentNames.DevelopmentRemote => new DevelopmentRemoteBuilder(), DefaultEnvironmentNames.Staging => new StagingBuilder(), DefaultEnvironmentNames.Production => new ProductionBuilder(), _ => throw new System.ArgumentException($"{nameof(environmentName)}") }; }
で大丈夫。
varで受ける場合
varで受ける場合は、いつもの型推論の制約で、抽象的な型で受けられません。なのでこうなります。
なので、varで受け取る場合に、抽象的な型で受け取りたい場合はキャストが結局必要です。
static IEnvironmentBuilder GetEnvironmentBuilder(string environmentName) { var environmentBuilder = environmentName switch { DefaultEnvironmentNames.Development => (IEnvironmentBuilder) new DevelopmentBuilder(), DefaultEnvironmentNames.DevelopmentRemote => new DevelopmentRemoteBuilder(), DefaultEnvironmentNames.Staging => new StagingBuilder(), DefaultEnvironmentNames.Production => new ProductionBuilder(), _ => throw new System.ArgumentException($"{nameof(environmentName)}") }; return environmentBuilder; }
ローカル変数で受ける場合
varで受ける場合~の話と、推論の話で、もう予想はつくと思いますが、ローカル変数でswitch式を受ける場合はキャストは不要です。
static IEnvironmentBuilder GetEnvironmentBuilder(string environmentName) { IEnvironmentBuilder environmentBuilder; environmentBuilder = environmentName switch { DefaultEnvironmentNames.Development => new DevelopmentBuilder(), DefaultEnvironmentNames.DevelopmentRemote => new DevelopmentRemoteBuilder(), DefaultEnvironmentNames.Staging => new StagingBuilder(), DefaultEnvironmentNames.Production => new ProductionBuilder(), _ => throw new System.ArgumentException($"{nameof(environmentName)}") }; return environmentBuilder; }
switch式、後置き構文なこともあって、switch ステートメントよりもずいぶん読みやすくなりました。 使える時には積極的に使っていきましょう。
Microsoft.AspNetCore.Appメタパッケージを利用してライブラリーを作成する際のProject Sdkについて見てみる
Microsoft.AspNetCore.Appメタパッケージを使いたい
Webゾーンに関わるライブラリーを書く場合、Microsoft.AspNetCore.Appメタパッケージを参照したくなります。 .NET Coreの.csprojを編集するときに、Project SdkをMicrosoft.NET.Sdk.Webに設定するとDefaultで
Microsoft.AspNetCore.App
Microsoft.NetCore.App
の二つがFramework参照されます。
.csprojの中のProject Sdkはこんな感じ
<Project Sdk="Microsoft.NET.Sdk.Web">
すると、Framework参照はされていますが、Projectのプロパティのうち、アプリケーション-出力の種類 がコンソールアプリケーションになっています。
「おや?」と思い、クラスライブラリーに変更し、今度はパッケージ-ビルドでNugetパッケージを生成 にチェックをつけてビルドしてみます。
「おや?.nupkgが生成されないぞ?」
.csprojの中のProject SdkをMicrosoft.NET.Sdkに戻す
<Project Sdk="Microsoft.NET.Sdk">
これだけだと、Framework参照が足りないので、Microsoft.AspNetCore.Appを(2.2.7なんだよなーと思いながら)パッケージ参照に追加します。
Framework参照にしろよ!って怒られる
FrameworkReferenceに書き換える
<ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> </ItemGroup>
まとめ
Microsoft.AspNetCore.Appメタパッケージを利用してのライブラリーを書くときは、Project Sdkを変更するのではなく、FrameworkReferenceを追加しましょう。(意外と悩みました。)
IAsyncDisposableを実装したObjectをASP.NET CoreのDIでライフタイム管理するときの挙動を見てみる
Using IAsyncDisposable
C#8から追加されたIAsyncDisposable、言わずと知れた await using ~ を実現するための存在ですが、ASP.NET Coreで開発している場合、各Objectのライフタイム管理は、DIContainerに任せることが大半だと思います。 DIContainerにライフタイム管理を任せる場合に、IAsyncDisposableを実装した場合どうなるかを見ていきます。
SqlManager
とりあえずインジェクションしたいのでインジェクションするためにObjectを作ります。
本当はIAsyncDisposableの実装だけにしたいんですが、IDisposable の実装が優先される可能性があるかも見たいので今回は両方実装してます。
using Dapper; using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading.Tasks; namespace MicroORMWrapper { public class SqlManager : IAsyncDisposable, IDisposable { private DbConnection DbConnection { get; set; } public bool IsOpenedConnection => DbConnection.State == ConnectionState.Open; public SqlManager(DbConnection dbConnection) { DbConnection = dbConnection; OpenConnection(); } public void OpenConnection() { if (IsOpenedConnection) { return; } DbConnection.Open(); } public async ValueTask CloseConnectionAsync() { if (!IsOpenedConnection) { return; } DbConnection.CloseAsync(); } public IEnumerable<TResult> Select<TResult>(string query) where TResult : class? { return DbConnection.Query<TResult>(query); } public IEnumerable<TResult> Select<TResult>(string query, object prameters) where TResult : class? { return DbConnection.Query<TResult>(query, prameters); } public IEnumerable<TResult> Select<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? { return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn); } public IEnumerable<TResult> Select<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? { return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn); } public Task<IEnumerable<TResult>> SelectAsync<TResult>(string query) where TResult : class? { return DbConnection.QueryAsync<TResult>(query); } public Task<IEnumerable<TResult>> SelectAsync<TResult>(string query, object prameters) where TResult : class? { return DbConnection.QueryAsync<TResult>(query, prameters); } public Task<IEnumerable<TResult>> SelectAsync<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? { return DbConnection.QueryAsync(query, includeFunc, prameters, null, true, splitOn); } public Task<IEnumerable<TResult>> SelectAsync<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? { return DbConnection.QueryAsync(query, includeFunc, prameters, null, true, splitOn); } public List<TResult> SelectAsList<TResult>(string query) where TResult : class? { return DbConnection.Query<TResult>(query).AsList(); } public List<TResult> SelectAsList<TResult>(string query, object prameters) where TResult : class? { return DbConnection.Query<TResult>(query, prameters).AsList(); } public List<TResult> SelectAsList<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? { return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn).AsList(); } public List<TResult> SelectAsList<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? { return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn).AsList(); } public BuiltInType GetValue<BuiltInType>(string query) { return DbConnection.ExecuteScalar<BuiltInType>(query); } public BuiltInType GetValue<BuiltInType>(string query, object prameters) { return DbConnection.ExecuteScalar<BuiltInType>(query, prameters); } public Task<BuiltInType> GetValueAsync<BuiltInType>(string query) { return DbConnection.ExecuteScalarAsync<BuiltInType>(query); } public Task<BuiltInType> GetValueAsync<BuiltInType>(string query, object prameters) { return DbConnection.ExecuteScalarAsync<BuiltInType>(query, prameters); } public int Execute(string query) { return DbConnection.Execute(query); } public int Execute(string query, object prameters) { return DbConnection.Execute(query, prameters); } public Task<int> ExecuteAsync(string query) { return DbConnection.ExecuteAsync(query); } public Task<int> ExecuteAsync(string query, object prameters) { return DbConnection.ExecuteAsync(query, prameters); } public async ValueTask DisposeAsync() { await CloseConnectionAsync(); GC.SuppressFinalize(this); } public void Dispose() { CloseConnection(); GC.SuppressFinalize(this); } public void CloseConnection() { if (!IsOpenedConnection) { return; } DbConnection.Close(); } } }
DI Settings
services.AddControllers(); services .AddScoped<DbConnection>((servicProvider) => new SqlConnection( [SettingPoint:ConnectionString] )) .AddScoped<SqlManager>();
SqlManagerはDbConnectionをコンストラクターインジェクションで受け取りたいので、それぞれ、PerRequestで追加します。
Execute
結論から言うと、ASP.NET Core Mvcの場合、IAsyncDisposableを実装している場合は、IDisposableは無視するようで、Disposeメソッドのブレイクには引っ掛かりません。 なので、100%DI管理下での利用だとわかっている場合は、IDisposableの実装はASP.NET Core MVCでは不要ということになります。
Remarks
もちろん普通にawait using、usingでの宣言を行う場合は、IAsyncDisposable, IDisposableがそれぞれ必要になります。
//こっちで書く場合はIAsyncDisposableの実装が必要 await using var sqlManager= new SqlManager(new SqlConnection( [SettingPoint:ConnectionString] ));
//こっちで書く場合はIDisposableの実装が必要 using var sqlManager= new SqlManager(new SqlConnection( [SettingPoint:ConnectionString] ));
どちらでもできるようにするためには、両方のインターフェースの実装が必要になりますが、そもそもコンソールアプリとかだと、async Mainも使えるので、一旦はIAsyncDisposableのみ実装して、必要に迫られたらIDisposableを実装するでいい気がしてます。 というのも、同期版があると、やっぱりまだまだ皆さん同期版を使ってしまう印象なので。