3

どの Web アプリケーションがロード バランサーからトラフィックを処理するかを制御する Web アプリケーションがあります。Web アプリケーションは、個々のサーバーで実行されます。

ASP.NET アプリケーション状態のオブジェクト内の各アプリケーションの "in または out" 状態を追跡し、状態が変更されるたびに、オブジェクトはディスク上のファイルにシリアル化されます。状態は、Web アプリケーションの開始時にファイルから逆シリアル化されます。

サイト自体は 1 秒に数件のリクエストしか取得せず、ファイルへのアクセスはめったにありませんが、何らかの理由で、ファイルの読み取りまたは書き込み中に衝突が非常に発生しやすいことがわかりました。サーバーへのローリング展開を定期的に行う自動化されたシステムがあるため、このメカニズムは非常に信頼できるものである必要があります。

上記のいずれかの慎重さに疑問を呈するコメントをする前に、その背後にある理由を説明すると、この投稿がこれまでよりもずっと長くなってしまうので、山を動かすことは避けたいと思います.

つまり、ファイルへのアクセスを制御するために使用するコードは次のようになります。

internal static Mutex _lock = null;
/// <summary>Executes the specified <see cref="Func{FileStream, Object}" /> delegate on 
/// the filesystem copy of the <see cref="ServerState" />.
/// The work done on the file is wrapped in a lock statement to ensure there are no 
/// locking collisions caused by attempting to save and load the file simultaneously 
/// from separate requests.
/// </summary>
/// <param name="action">The logic to be executed on the 
/// <see cref="ServerState" /> file.</param>
/// <returns>An object containing any result data returned by <param name="func" />. 
///</returns>
private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = new Logger();
    if (ServerState._lock.WaitOne(1500, false))
    {
        l.LogInformation( "Got lock to read/write file-based server state."
                        , (Int32)VipEvent.GotStateLock);
        var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate 
                                  , FileAccess.ReadWrite, FileShare.None);                
        result = func.Invoke(fileStream);                
        fileStream.Close();
        fileStream.Dispose();
        fileStream = null;
        ServerState._lock.ReleaseMutex();
        l.LogInformation( "Released state file lock."
                        , (Int32)VipEvent.ReleasedStateLock);
        return true;
    }
    else
    {
        l.LogWarning( "Could not get a lock to access the file-based server state."
                    , (Int32)VipEvent.CouldNotGetStateLock);
        result = null;
        return false;
    }
}

これは通常は機能しますが、ミューテックスにアクセスできない場合があります (ログに「ロックを取得できませんでした」というイベントが表示されます)。これをローカルで再現することはできません。実稼働サーバー (Win Server 2k3/IIS 6) でのみ発生します。タイムアウトを削除すると、後続のリクエストを含め、アプリケーションが無期限にハングします (競合状態??)。

エラーが発生した場合、イベント ログを見ると、ミューテックス ロックが達成され、エラーがログに記録される前に以前の要求によって解放されたことがわかります。

ミューテックスは Application_Start イベントでインスタンス化されます。宣言で静的にインスタンス化すると、同じ結果が得られます。

言い訳、言い訳: スレッド化/ロックは私の得意分野ではありません。通常は気にする必要がないからです。

ランダムに信号を取得できない理由について何か提案はありますか?


アップデート:

適切なエラー処理を追加しましたが (恥ずかしいことです!)、それでも同じエラーが発生します。記録としては、未処理の例外が問題になることはありませんでした。

ファイルにアクセスするプロセスは 1 つだけです。このアプリケーションの Web プールに Web ガーデンは使用せず、他のアプリケーションはファイルを使用しません。私が考えることができる唯一の例外は、アプリ プールがリサイクルされ、新しい WP が作成されたときに古い WP がまだ開いている場合ですが、タスク マネージャーを見て、ワーカー プロセスが 1 つしかないときに問題が発生することがわかります。

@mmr: Monitor の使用と Mutex の使用の違いは何ですか? MSDN のドキュメントに基づくと、効果的に同じことを行っているように見えます。Mutex でロックを取得できない場合は、falseを返すだけで正常に失敗します。

注意すべきもう 1 つの点: 私が抱えている問題は完全にランダムなようです。1 つの要求で失敗した場合、次の要求ではうまくいく可能性があります。パターンもないようです (確かに、少なくとも他のすべてではありません)。


更新 2:

このロックは、他の呼び出しには使用されません。_lock が InvokeOnFile メソッドの外部で参照されるのは、インスタンス化されるときだけです。

呼び出される Func は、ファイルから読み取ってオブジェクトに逆シリアル化するか、オブジェクトをシリアル化してファイルに書き込みます。どちらの操作も別のスレッドでは行われません。

ServerState.PATH は静的な読み取り専用フィールドであり、同時実行の問題が発生することはないと思います。

また、これをローカル (Cassini) で再現できないという以前の指摘を繰り返したいと思います。


学んだ教訓:

  • 適切なエラー処理を使用してください (当たり前!)
  • 仕事に適したツールを使用してください(そして、そのツールが何をどのように行うかについての基本的な理解を持ってください). sambo が指摘しているように、Mutex を使用すると明らかに多くのオーバーヘッドが発生し、それが私のアプリケーションで問題を引き起こしていましたが、Monitor は .NET 専用に設計されています。
4

3 に答える 3

16

プロセス間の同期が必要な場合にのみ、ミューテックスを使用する必要があります。

ミューテックスはプロセス内のスレッド同期に使用できますが、モニターは.NET Framework専用に設計されているため、リソースをより有効に活用できるため、通常はモニターを使用することをお勧めします。対照的に、MutexクラスはWin32構造のラッパーです。モニターよりも強力ですが、ミューテックスには、Monitorクラスで必要とされるものよりも計算コストの高い相互運用遷移が必要です。

プロセス間ロックをサポートする必要がある場合は、グローバルミューテックスが必要です。

使用されているパターンは非常に壊れやすく、例外処理はなく、Mutexがリリースされていることを保証していません。これは本当に危険なコードであり、タイムアウトがないときにこれらがハングするのはおそらく理由です。

また、ファイル操作に1.5秒以上かかる場合は、同時ミューテックスがそれを取得できない可能性があります。ロックを正しく行い、タイムアウトを回避することをお勧めします。

これを書き直してロックを使うのが一番いいと思います。また、別のメソッドを呼び出しているようです。これが永遠にかかる場合、ロックは永久に保持されます。それはかなり危険です。

これは短く、はるかに安全です。

// if you want timeout support use 
// try{var success=Monitor.TryEnter(m_syncObj, 2000);}
// finally{Monitor.Exit(m_syncObj)}
lock(m_syncObj)
{
    l.LogInformation( "Got lock to read/write file-based server state."
                    , (Int32)VipEvent.GotStateLock);
    using (var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate
                                     , FileAccess.ReadWrite, FileShare.None))
    {
        // the line below is risky, what will happen if the call to invoke
        // never returns? 
        result = func.Invoke(fileStream);
    }
}

l.LogInformation("Released state file lock.", (Int32)VipEvent.ReleasedStateLock);
return true;

// note exceptions may leak out of this method. either handle them here.
// or in the calling method. 
// For example the file access may fail of func.Invoke may fail
于 2009-01-15T21:40:26.513 に答える
2

一部のファイル操作が失敗した場合、ロックは解除されません。おそらくそうです。ファイル操作をtry/catchブロックに入れ、finallyブロックのロックを解除します。

とにかく、Global.asax Application_Startメソッドでファイルを読み取る場合、これにより、他の誰もそのファイルで作業していないことが保証されます(ファイルはアプリケーションの起動時に読み取られると言いましたよね?)。アプリケーションプールの再起動などでの衝突を回避するには、ファイルの読み取りを試行し(書き込み操作で排他ロックが使用されると想定)、1秒間待ってから、例外がスローされた場合は再試行します。

ここで、書き込みの同期に問題があります。ファイルを変更することを決定するメソッドが何であれ、単純なロックステートメントで別のメソッドが進行中の場合は、書き込み操作を呼び出さないように注意する必要があります。

于 2009-01-15T21:47:02.770 に答える
0

ここにはいくつかの潜在的な問題があります。

更新 2 の編集: 関数が単純なシリアル化/逆シリアル化の組み合わせである場合、2 つを 2 つの異なる関数に分割します。1 つは「シリアル化」関数、もう 1 つは「逆シリアル化」関数です。これらは実際には 2 つの異なるタスクです。その後、さまざまなロック固有のタスクを実行できます。Invoke は気の利いたものですが、私自身、「働く」よりも「気の利いた」ものを選ぶのに苦労しました。

1) LogInformation 機能がロックされていますか? 最初にミューテックス内で呼び出してから、一度ミューテックスを解放するためです。したがって、ログ ファイル/構造に書き込むためのロックがある場合は、そこで競合状態になる可能性があります。それを避けるには、ログをロックの中に入れます。

2) Monitor クラスを使用してチェックアウトします。これは C# で機能することがわかっており、ASP.NET でも機能すると思います。そのためには、単にロックを取得しようとするだけでよく、それ以外の場合は正常に失敗します。これを使用する 1 つの方法は、ロックを取得しようとし続けることです。(理由の編集:ここを参照してください。基本的に、ミューテックスはプロセス全体にあり、モニターは1つのプロセスにあるだけですが、.NET用に設計されているため、優先されます。ドキュメントでは他の実際の説明はありません。)

3) 他の誰かがロックを持っているためにファイルストリームのオープンに失敗した場合はどうなりますか? これにより、例外がスローされ、このコードの動作が悪くなる可能性があります (つまり、ロックは例外を持つスレッドによって保持されたままになり、別のスレッドがそれを取得できます)。

4) 関数自体はどうですか? それは別のスレッドを開始しますか、それとも完全に1つのスレッド内にありますか? ServerState.PATH へのアクセスはどうですか?

5) ServerState._lock にアクセスできる他の関数は? 競合/デッドロック状態を回避するために、ロックを必要とする各関数に独自のロックを取得させることを好みます。多くのスレッドがあり、それぞれがまったく異なるタスクのために同じオブジェクトをロックしようとすると、簡単に理解できる理由がなくても、デッドロックや競合が発生する可能性があります。グローバルロックを使用するのではなく、その考えを反映するコードに変更しました。(他の人がグローバルロックを提案していることに気づきました。このタスクではないタスクのために他のものがそれをつかむ可能性があるため、そのアイデアは本当に好きではありません)。

    Object MyLock = new Object();
    private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = null;
    var filestream = null;
    Boolean success = false;
    if (Monitor.TryEnter(MyLock, 1500))
        try {
            l = new Logger();
            l.LogInformation("Got lock to read/write file-based server state.", (Int32)VipEvent.GotStateLock);
            using (fileStream = File.Open(ServerState.PATH, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)){                
                result = func.Invoke(fileStream); 
            }    //'using' means avoiding the dispose/close requirements
            success = true;
         }
         catch {//your filestream access failed

            l.LogInformation("File access failed.", (Int32)VipEvent.ReleasedStateLock);
         } finally {
            l.LogInformation("About to released state file lock.", (Int32)VipEvent.ReleasedStateLock);
            Monitor.Exit(MyLock);//gets you out of the lock you've got
        }
    } else {
         result = null;
         //l.LogWarning("Could not get a lock to access the file-based server state.", (Int32)VipEvent.CouldNotGetStateLock);//if the lock doesn't show in the log, then it wasn't gotten; again, if your logger is locking, then you could have some issues here
    }
  return Success;
}
于 2009-01-15T21:53:08.937 に答える