音楽サーバ"Mopidy"のフロントエンドを作る:14 Linuxでハマりやすいところ
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第14回です。
今回は、Asp.Net CoreアプリをLinuxで動かす際にハマるポイントを追っていきます。
ソケットは自動で破棄されない
C#の動作環境である.Net VMはガベージコレクションが付いてます。
なので、インスタンスの破棄を意識しないで書いていても、それなりに動いてくれます。
しかし、LinuxのSocket周りには注意が必要です。
Windows機の場合は、参照されなくなったSocketはガベコレが回収してくれます。
ところがLinuxでは、意図して破棄しないと、Socketをすぐに使い切ってしまいます。
昨今のC#では、TCP/UDPのSocketを生成する場面は少ないかもしれません。
しかし、例えばHttpClient
。
外部のWebサービスを使う際に便利な実装ですが、内部的にはSocketが生成されます。
これをDisposeしないまま放置しておくと、LinuxではやがてSocketを使い切ってしまい、接続出来なくなってしまいます。
例えば、こんな検証コードを書いてみました。
public class Program { // Set your Mopidy Address private const string MopidyRpcUrl = "http://192.168.254.251:6680/mopidy/rpc"; private static int _count = 0; public static void Main(string[] args) { var withUsing = false; if (args.Contains("--withoutusing")) withUsing = false; else if (args.Contains("--withusing")) withUsing = true; Console.WriteLine("Start Socket Overflow Test: " + ((withUsing) ? "with Using" : "without Using")); while (true) { try { Program._count++; var sendJson = $"{{\"jsonrpc\": \"2.0\", \"method\":\"core.playback.get_state\", \"id\": {Program._count}}}"; var content = new StringContent(sendJson, Encoding.UTF8, "application/json"); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine($"Try {((withUsing) ? "with Using" : "without Using")} Count: " + Program._count.ToString()); Console.WriteLine("Send Message: " + sendJson); var result = (withUsing) ? Program.GetResultWithUsing(content) : Program.GetResultWithoutUsing(content); Console.WriteLine("Result: " + result); Task.Delay(200).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine($"Exception!"); Program.DumpException(ex); Task.Delay(1000).GetAwaiter().GetResult(); } } } private static string GetResultWithUsing(StringContent content) { using (var client = Program.GetHttpClient()) { var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult(); var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult(); return result; } } private static string GetResultWithoutUsing(StringContent content) { var client = Program.GetHttpClient(); var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult(); var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult(); return result; } private static HttpClient GetHttpClient() { var result = new HttpClient(); result.DefaultRequestHeaders.Accept.Clear(); result.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json") ); result.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); result.Timeout = TimeSpan.FromMilliseconds(1000); return result; } private static void DumpException(Exception ex) { Console.WriteLine("----------------------------------------"); Console.WriteLine($"Message: {ex.Message}"); Console.WriteLine($"StackTrace: {ex.StackTrace}"); if (ex.InnerException != null) Program.DumpException(ex.InnerException); } }
MopidyのJSON-RPCに、単純なクエリを繰り返すコードです。
引数を"--withusing"とすると、HttpClientをusingでラップして破棄するように。
"--withouusing"とすると、破棄しないで放置するようにしています。
このコードで、linux用バイナリを生成します。
# dotnet publish -c Release -r linux-x64
これをUbuntu18.0.4LTSで実行してみます。
ミニマルインストール後に、SSHと、実行ファイルコピー用のSambaを入れました。
まず、"--withoutusing"スイッチを付けて実行してみると...
接続回数1004回目で、落ちてしまいます。
CentOS7.6でも試してみます。
やはり、1004回目で落ちますね。
そして、"--withusing"スイッチを付けて実行してみると。
Ubuntu18では...
問題なし、です。
CentOS7.6でも、問題ありません。
Windowsでも実行してみます。
まずば実行バイナリの生成。
# dotnet publish -c Release -r win-x64
そして、Linuxでは落ちてしまう"--withoutusing"スイッチを付けて実行すると...
こちらは、問題なく動き続けてしまいます。
このように、Windowsでは動作に問題ないコードであっても、Linuxでは動かなくなることがあるんですね。
C#で通信している箇所は要注意、と見ておいたほうが良いでしょう。
一部NuGetパッケージのバージョン暗黙解釈に失敗する
下記は、Asp.NetCore2.2-MVCプロジェクト生成直後のcsprojファイルです。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> </ItemGroup> </Project>
Microsoft.AspNetCore.App
に、バージョンが書かれていないんですね。
しかし、NuGetパッケージマネージャで見てみると。
ここではきちんと、v2.2.0
と表記があります。
どうやら、バージョンを暗黙的に解釈しているようです。
しかしこのままコードを作り進み、Linuxに持って行ってpublishしてみると...
[umuser@ume01srv tmp]$ dotnet publish -c Release -r linux-x64 Microsoft (R) Build Engine version 16.1.76+g14b0a930a7 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Design from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Design (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore.Design (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Tools from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version. [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.6 && < 2.3.0) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: MopidyFinder -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln] Restore failed in 1.13 sec for /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj.
こんなふうに、NuGetパッケージの整合性が取れずにエラーになることがあります。
この場合は、csprojファイル上のMicrosoft.AspNetCore.App
パッケージに、バージョンを書き加えます。
<PackageReference Include="Microsoft.AspNetCore.App" />
↓
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />
これでpublishが通るようになりました。
今のところ、Microsoft.AspNetCore.App
以外で、バージョン表記が無いという現象に行き当たったことはありません。
が、時々はcsprojファイルの中身もチェックしといた方が良さそうです。
パス文字列の大文字/小文字
これは言語に関わらず、出てくる問題ですね。
Webプログラミングをやっていれば必ず直面する問題のため、みなさま恐らく普段から気をつけていらっしゃると思います。
私も気を付けてはいたのですが...。
Mopidy.Finderでも、一度直面しました。こちらのコミットです。
キャメルケースのフォルダ名を「Sidebars」→「SideBars」と変更した際の現象です。
Windows上では「SideBars」に変わっていたのですが、WindowsのGitクライアントがその違いを認識できておらず。
Gitリポジトリ上ではずっと、「Sidebars」として保持されていました。
TypeScriptのコンパイルをLinux上で実行することは滅多に無かったため、リリース直前まで気が付きませんでした。
いやはや、お恥ずかしい。
RaspberryPi(Raspbian)は32bitOS
最近のラズパイのCPUは既に64bit化されているのですが、OSはまだ32bit版なんですね。
CentOSやUbuntuで動かしたバイナリをそのままコピペしてしまい、起動しない原因が分からずにしばらく悩みました。
Raspbianで動かすためのバイナリを作るには、publishの引数を下記のようにします。
# dotnet publish -c Release -r linux-arm
でも、乗り越えてしまえば
こまごまと問題に直面することも、あるとはいえ。
それらを乗り切ってしまえば、.Net Coreアプリは快調に動いてくれます。
最近のpublishの処理は、大変優秀です!
ミニマルインストールしたLinuxにバイナリをコピペするだけで、すんなりと動きます。
C#erとしては、まったく良い時代になったもんだ、としみじみ思います。
以上、Linuxのハマりポイントのおはなし、でした!
これで、予定していた記事を全て書き終えました。
またC#ネタが出てきたら、適当に書き連ねようと思います。
長々と駄文にお付き合いいただき、ありがとうございました!