Goはどのようにしてホットリスタートを実現しますか

著者:zhijiezhang、TencentPCGバックグラウンド開発エンジニア

最近、会社のフレームワークtrpcを最適化するときに、ホットリスタートに関連する問題を発見しました。最適化後、要約して解決し、外出先でホットリスタートを実現する方法を簡単に確認しました。

1.ホットリスタートとは何ですか?

ホットリスタート(ホットリスタート)は、サービスの可用性を確保するための手段です。これにより、確立された接続がサービスの再起動中に中断されることがなくなり、古いサービスプロセスは新しい接続要求を受け入れなくなり、新しい接続要求は新しいサービスプロセスで受け入れられます。元のサービスプロセスで確立された接続については、読み取りを行うように設定し、接続の要求がスムーズに処理され、接続がアイドル状態になるのを待ってから終了することもできます。このようにして、確立された接続が中断されないこと、接続でのトランザクション(要求、処理、応答)が正常に完了できること、および新しいサービスプロセスが接続を受け入れて接続での要求を正常に処理できることを保証できます。もちろん、ホットリスタート中のプロセスのスムーズな終了には、接続されたトランザクションだけでなく、注意が必要なメッセージサービスやカスタムトランザクションも含まれます。

私が理解しているように、これはホットリスタートの大まかな説明です。今すぐウォームリスタートする必要がありますか?私の理解は、シーンを見ることです。

バックグラウンド開発を例として取り上げます。運用および保守プラットフォームに、サービスのアップグレードまたは再起動時にトラフィックを自動的に開始し、サービスの準備ができたときにトラフィックを自動的に追加する機能がある場合、サービスのQPSと要求の処理時間を合理的に見積もることができる場合は、適切な構成を行うだけです。停止するまでの待機時間は、ホットリスタートと同様の効果を実現できます。この場合、バックグラウンドサービスでホットリスタートをサポートする必要はありません。ただし、マイクロサービスフレームワークを開発する場合、将来の展開プラットフォームと環境についてそのような仮定を立てることはできません。また、ユーザーが他の負荷分散機能なしで1つまたは2つの物理マシンにのみ展開する可能性もありますが、再起動の影響を受けたくありません。干渉、ホットリスタートが必要です。もちろん、ホットリスタート機能も必要とする、より複雑で要求の厳しいシナリオがいくつかあります。

ホットリスタートは、サービス品質を確保するためのより重要な手段であり、理解する価値があります。これは、この記事の本来の目的でもあります。

2.ホットリスタートを実現するにはどうすればよいですか?

実際、ホットリスタートを実現する方法はここでは一般化できませんが、実際のシナリオ(サービスプログラミングモデル、可用性要件など)と組み合わせる必要があります。大まかな実現のアイデアを最初に投げることができます。

一般に、ホットリスタートを実現するには、おおよそ次の手順を含める必要があります。

  • まず、ここでは親プロセスと呼ばれる古いプロセスで、最初に子プロセスをフォークして置き換えます。

  • 次に、子プロセスの準備ができたら、親プロセスに通知し、新しい接続要求を通常どおり受け入れ、接続で受信した要求を処理します。

  • 次に、親プロセスが確立された接続で要求を処理し、接続がアイドル状態になると、スムーズに終了します。

シンプルに聞こえます...

2.1。フォークを知る

誰もがfork()システム呼び出しを知っており、forkを呼び出す親プロセスはプロセスのコピーを作成します。コードは、フォークの戻り値が0であるかどうかによって、それが子プロセスであるか親プロセスであるかを区別することもできます。

int main(char **argv, int argc) {
    pid_t pid = fork();
    if (pid == 0) {
        printf("i am child process");
    } else {
        printf("i am parent process, i have a child process named %d", pid);
    }
}

一部の開発者は、forkの実装原理、forkの戻り値が親子プロセスで異なる理由、または親子プロセスの戻り値を異なるようにする方法を知らない場合があります...これらを理解するには、少しの知識の蓄積が必要です。

2.2。戻り値

簡単にまとめると、ABIは、関数呼び出し、パラメーターの受け渡し方法、値を返す方法などのいくつかの仕様を定義します。x86を例にとると、戻り値がraxレジスターに含まれている場合、通常はraxレジスターを介して返されます。

raxレジスタのビット幅が戻り値に対応できない場合はどうなりますか?また、コンパイラはこれらの不思議な操作を完了するためにいくつかの命令を挿入します。特定の命令は言語コンパイラの実装に関連しています。

  • c言語では、戻り値のアドレスをrdiまたは他のレジスタに渡すことができます。呼び出された関数内で、戻り値は複数の命令を介してrdiによって参照されるメモリ領域に書き込まれます。

  • c言語では、複数のレジスタrax、rdx ...を使用して、返された結果を呼び出された関数に一時的に保存し、関数が戻ったときに複数のレジスタの値を変数に割り当てることもできます。

  • また、golangのようなスタックメモリを介して戻る場合もあります。

2.3。フォークの戻り値

forkシステム呼び出しの戻り値は少し特殊です。親プロセスと子プロセスでは、この関数の戻り値が異なります。どうすればよいですか?

Lenovoが親プロセスからフォークを呼び出す場合、オペレーティングシステムカーネルは何をする必要がありますか?プロセス制御ブロックの割り当て、pidの割り当て、メモリスペースの割り当て...多くのことが必要です。ここでは、プロセスのハードウェアコンテキスト情報に注意してください。これらは非常に重要です。プロセスがスケジューリングのスケジューリングアルゴリズムによって選択される場合、ハードウェアコンテキスト情報を復元する必要があります。の。

Linuxがフォークすると、子プロセスのハードウェアコンテキストに特定の変更が加えられます。フォーク後のpidを0にするだけです。どうすればよいですか?前のセクション2.2で述べたように、これらの小さい整数の場合、raxレジスタは十分すぎるほどです。forkが戻ると、オペレーティングシステムによって割り当てられたpidがraxレジスタに配置されます。

次に、子プロセスの場合、フォーク時にハードウェアコンテキストraxレジスタを0にクリアし、他の設定がすべて正常になるのを待ってから、状態を中断できない待機状態から実行可能な状態に変更し、それが完了するのを待つだけです。スケジューラーがスケジュールを設定しているとき、最初にPC、raxなどのハードウェアコンテキスト情報を復元します。フォークが戻った後、raxの中央値は0になり、pidに割り当てられる最終値は0になります。

したがって、「pidが0に等しい」かどうかを判断することにより、現在のプロセスが親プロセスであるか子プロセスであるかを区別することができます。

2.4。制限

多くの人は、フォークがプロセスのコピーを作成してそれを実行し続けることができ、フォークの戻り値に応じて異なる分岐ロジックを実行できることを知っています。プロセスがマルチスレッドの場合、1つのスレッドでforkを呼び出すと、プロセス全体がコピーされますか?

Forkは、関数を呼び出すスレッドのコピーのみを作成できます。プロセスで実行中の他のスレッドの場合、forkは処理されません。これは、マルチスレッドプログラムの場合、フォークを介してプロセスの完全なコピーを作成することを期待することは不可能であることを意味します。

先に述べたように、フォークはホットリスタートを実現するための重要な部分です。ここでのフォークの制限により、さまざまなサービスプログラミングモデルでのホットリスタートの実装が制限されます。したがって、特定の問題が詳細に分析され、さまざまな実装がさまざまなプログラミングモデルで実際に使用できると言えます。

3.シングルプロセスシングルスレッドモデル

シングルプロセスシングルスレッドモデルは、多くの人にとって時代遅れと見なされる可能性があり、実稼働環境では使用できません。シングルスレッドだけでなく、redisよりも強力です。シングルスレッドモデルは役に立たないわけではないことを強調し、それを取り戻して、シングルプロセスのシングルスレッドモデルがどのようにホットリスタートを実現できるかに焦点を当てます。

シングルプロセスとシングルスレッドの場合、ホットリスタートを実装する方が簡単です。

  • フォークを使用して子プロセスを作成できます。

  • 子プロセスは、親プロセスのlistenfdやconnfdなど、開いているファイル記述子など、親プロセスのリソースを継承できます。

  • 親プロセスはlistenfdを閉じることを選択でき、接続を受け入れる後続のタスクは子プロセスに渡されて完了します。

  • 親プロセスはconnfdを閉じることもでき、子プロセスが接続要求を処理したり、パケットを返したりすることができます。また、確立された接続で要求を単独で処理することもできます。

  • 親プロセスは適切な時点で終了することを選択し、子プロセスが柱になり始めます。

コアアイデアはこれらですが、実現に関しては、多くの方法があります。

  • forkメソッドを選択して、子プロセスが元のlistenfd、connfd、を取得できるようにすることができます。

  • unixdomainソケットを選択することもできます。親プロセスはlistenfd、connfdを子プロセスに送信します。

一部の学生は、これらのfdを渡すことができないと思うかもしれません。

  • たとえば、reuseportを開くと、親プロセスは確立された接続connfdで要求を直接処理してから閉じます。子プロセスのreuseport.Listenは、新しいlistenfdを直接作成します。

はい!ただし、いくつかの問題は事前に検討する必要があります。

  • 再利用ポートでは、複数のプロセスが同じポートで複数回リッスンできますが、要件を満たしているようですが、euidが同じである限り、このポートでリッスンできることを知っておく必要があります。安全ではありません!

  • 再利用ポートの実装はプラットフォームに関連しています。Linuxプラットフォームでは、同じアドレス+ポートで複数回リッスンでき、複数のlistenfd最下位層が同じ接続キューを共有できます。カーネルは負荷分散を実現できますが、ダーウィンプラットフォームでは実現できません。

もちろん、ここで言及されている問題は、マルチスレッドモデルの下に確かに存在します。

4.シングルプロセスマルチスレッドモデル

前述の問題は、マルチスレッドモデルにも現れます。

  • フォークは呼び出しスレッドのみをコピーでき、プロセス全体はコピーできません。

  • 同じアドレス+ポートのreuseportによって取得された複数のfdは複数回リッスンし、プラットフォームが異なればパフォーマンスも異なり、接続を受け入れるときにロードバンランスを達成できない場合があります。

  • 再利用しない場合、繰り返し聞くと失敗します!

  • fdを渡さないでください。relistportを介して直接リッスンしてlistenfdを取得してください。安全ではありません。異なるサービスプロセスインスタンスが同じポートでリッスンする可能性があります。

  • 親プロセスのロジックはスムーズに終了し、listenfdを閉じ、connfdでの要求処理の終了を待ち、connfdを閉じます。すべてが正常に行われた後、親プロセスが終了し、子プロセスが主導権を握ります。

5.その他のスレッドモデル

他のスレッドは基本的に上記の3と4の実装または組み合わせを回避することはできず、対応する問題も同様であるため、繰り返しません。

6.ホットリスタートを達成するために移動します:トリガータイミング

ホットリスタートをトリガーするタイミングを選択する必要がありますが、いつトリガーする必要がありますか?オペレーティングシステムは、プロセスがカスタム信号処理を行うことを可能にする信号メカニズムを提供します。

プロセスkill -9を強制終了すると、通常、SIGKILLシグナルがプロセスに送信されます。このシグナルはキャプチャできません。また、SIGABORTもキャプチャできません。これにより、プロセスの所有者または特権の高いユーザーがプロセスの生死を制御し、より良い管理結果を得ることができます。

Killは、SIGUSR1、SIGUSR2、SIGINTなどの送信など、プロセスに他の信号を送信するためにも使用できます。プロセスはこれらの信号を受信し、それに応じて処理できます。ここで、SIGUSR1またはSIGUSR2を選択して、ホットリスタートのプロセスに通知できます。

go func() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.SIGUSR2)
    <- ch

    //接下来就可以做热重启相关的逻辑了
    ...
}()

7.ホットリスタートの判断方法

goプログラムを再起動すると、すべてのランタイム状態情報が新しくなります。自分が子プロセスであるかどうか、またはホットリスタートロジックを実行するかどうかをどのように判断できますか?親プロセスは、HOT_RESTART = 1の追加など、子プロセスの初期化時に環境変数を設定できます。

これには、最初に環境変数HOT_RESTARTが適切な場所で1であるかどうかをチェックし、trueの場合はホットリスタートロジックを実行し、そうでない場合は新しいスタートアップロジックを実行するコードが必要です。

8. ForkExec

現在のプロセスがSIGUSR2信号を受信した後にホットリスタートロジックを実行する場合は、syscall.ForkExec(...)を実行して子プロセスを作成する必要があります。goは、プロトコルのスケジュールを複数のスレッドに依存するcc ++とは異なることに注意してください。 Chengは当然マルチスレッドプログラムですが、NPTLスレッドライブラリを使用して作成するのではなく、cloneシステム呼び出しを使用しました。

前述のように、単にフォークする場合は、フォーク関数を呼び出すスレッドのみをコピーでき、プロセス内の他のスレッドでは何もできません。したがって、goのような自然なマルチスレッドプログラムの場合、最初から再起動して再実行する必要があります。したがって、go標準ライブラリによって提供される関数は、syscall.Forkではなくsyscall.ForkExecです。

9.ホットリスタートを達成するために移動します:listenfdを渡します

fdをgoに渡すには、いくつかの方法があります。親プロセスが子プロセスをフォークする場合は、fdを渡すか、後でunixドメインソケットに渡します。渡すのは実際にはファイル記述子ではなく、ファイル記述であることに注意してください。

添付されているのは、Unixのようなシステムでのファイル記述子、ファイルの説明、およびinode間の関係の図です。

Fdは小さいものから大きいものへと割り当てられます。親プロセスのfdは10であり、子プロセスに渡された後は10にならない場合があります。では、子プロセスに渡されるfdは予測可能ですか?予測できますが、お勧めしません。だから私はそれを達成するために2つの方法を提供します。

9.1 ForkExec + ProcAttr {ファイル:[] uintptr {}}

listenfdを渡すのは非常に簡単です。net.Listenerタイプの場合は、を使用tcpln := ln.(*net.TCPListener); file, _ := tcpln.File(); fd := file.FD()して、リスナーの基になるファイル記述に対応するfdを取得します。

ここでのfdは、基になるファイルの説明に対応する最初のfdではなく、dup2によってコピーされたfd(tcpln.File()が呼び出されたときに割り当てられる)であるため、基になるファイルの説明の参照数は次のようになります。 +1。ln.Close()を使用してリスニングソケットを閉じたい場合は、申し訳ありませんが、閉じることはできません。ここでは、file.Close()を実行して、新しく作成されたfdを閉じ、対応するファイル記述の参照カウントを-1にし、閉じるときに参照カウントが0であることを確認してから、正常に閉じる必要があります。

ホットリスタートを実現する場合、接続で受信したリクエストが処理されるのを待ってからプロセスを終了する必要があると想像してください。ただし、この期間中、親プロセスは新しい接続リクエストを受信できなくなります。ここでリスナーを正常に閉じることができない場合、目標はそれは達成できません。したがって、dupからのfdの処理は、ここではもっと注意する必要があります。忘れないでください。

OK、syscall.ProcAttr {Files:[] uintptr {}}について話しましょう。これが渡される親プロセスのfdです。たとえば、stdin、stdout、stderrを子プロセスに渡すには、これらの対応するものを転送する必要があります。 fdをos.Stdin.FD()、os.Stdout.FD()、os.Stderr.FD()に挿入します。今すぐlistenfdを渡したい場合は、file.FD()返されたfdを上に挿入する必要があります

子プロセスでこれらのfdを受け取った後、Unixのようなシステムは通常、fdを0、1、2、3の昇順で割り当てます。その後、stdin、stdout、stderrを除いて、渡されたfdは予測可能です。さらに2つのlistenfdを渡すと、これら2つのfdは3、4になると予測できます。これは通常、Unixのようなシステムで行われます。子プロセスは渡されたfdの数に応じて3からカウントを開始できます(たとえば、環境変数FD_NUM = 2を介して子プロセスに渡されます)。ああ、これら2つのfdは3、4。

親プロセスと子プロセスは、合意された順序で渡されるlistenfdのシーケンスを整理して、同じ合意に従って子プロセスの処理を容易にすることができます。もちろん、fdを使用してリスナーを再構築し、対応するリスナーネットワーク+アドレスを決定してリスナーを区別することもできます。どの論理サービスが対応するか。それはすべて可能です!

file.FD()によって返されるfdは非ブロッキングであるため、基になるファイルの説明に影響することに注意してください。リスナーを再構築する前に、まずリスナーをnonblock、syscall.SetNonBlock(fd)に設定してからfile, _ := os.NewFile(fd); tcplistener := net.FileListener(file)、またははいudpconn := net.PacketConn(file)、取得できます。 tcplistenerとudpconnのリスニングアドレスは、対応する論理サービスに関連付けられています。

前述のように、file.FD()は、基になるファイルの説明をブロッキングモードに設定します。ここで、net.FileListener(f)、net.PacketConn(f)がnewFileFd()-> dupSocket()を内部的に呼び出すことを追加します。いくつかの関数は、fdに対応するファイル記述を非ブロッキングに内部的にリセットします。リスナーに対応するファイルの説明は、親プロセスと子プロセスで共有されるため、非ブロッキングとして表示する必要はありません。

一部のマイクロサービスフレームワークは、サービスの論理サービスグル​​ープ化をサポートしています。GooglePB仕様は、複数のサービス定義もサポートしています。これは、Tencentのgoentおよびtrpcフレームワークでもサポートされています。

もちろん、ここでは、すべての人のために上記のすべての説明を含む完全なデモを作成することはしません。これは少しスペースです。これは例の要約バージョンのみです。他の読者は、興味があれば自分でコーディングしてテストできます。紙の上では浅すぎることを知っておくことが重要です。そのため、さらに練習する必要があります。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

  ln, err := net.Listen("tcp", "localhost:8888")
  if err != nil {
   panic(err)
  }

  wg := sync.WaitGroup{}
  wg.Add(1)
  go func() {
   defer wg.Done()
   for {
    ln.Accept()
   }
  }()

  tcpln := ln.(*net.TCPListener)
  f, err := tcpln.File()
  if err != nil {
   panic(err)
  }

  os.Setenv(envRestart, "1")
  os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

  _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
   Env:   os.Environ(),
   Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), f.Fd()},
   Sys:   nil,
  })
  if err != nil {
   panic(err)
  }
  log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())
  f.Close()
  wg.Wait()

 } else {

  v := os.Getenv(envListenFD)
  fd, err := strconv.ParseInt(v, 10, 64)
  if err != nil {
   panic(err)
  }
  log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

  // case1: 理解上面提及的file descriptor、file description的关系
  // 这里子进程继承了父进程中传递过来的一些fd,但是fd数值与父进程中可能是不同的
  // 取消注释来测试...
  //ff := os.NewFile(uintptr(fd), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  log.Println(err)
  // }
  //}

  // case2: 假定父进程中共享了fd 0\1\2\listenfd给子进程,那再子进程中可以预测到listenfd=3
  ff := os.NewFile(uintptr(3), "")
  fmt.Println("fd:", ff.Fd())
  if ff != nil {
   _, err := ff.Stat()
   if err != nil {
    panic(err)
   }

   // 这里pause, 运行命令lsof -P -p $pid,检查下有没有listenfd传过来,除了0,1,2,应该有看到3
   // ctrl+d to continue
   ioutil.ReadAll(os.Stdin)

   fmt.Println("....")
   _, err = net.FileListener(ff)
   if err != nil {
    panic(err)
   }

   // 这里pause, 运行命令lsof -P -p $pid, 会发现有两个listenfd,
   // 因为前面调用了ff.FD() dup2了一个,如果这里不显示关闭,listener将无法关闭
   ff.Close()

   time.Sleep(time.Minute)
  }

  time.Sleep(time.Minute)
 }
}

これは、ProcAttrを使用してlistenfdを渡す方法を大まかに説明する簡単なコードです。ここに質問があります。たとえば、stdin、stdout、およびstderrのfdが渡されないなど、後続の親プロセスで渡されたfdが変更された場合はどうなりますか?サーバーは、0から番号付けを開始する必要があると予測し始めますか?環境変数を介して子プロセスに通知できます。たとえば、渡されたfdがlistenfdである番号から、複数のlistenfdがあるため、これも実現可能です。

この実装は、クロスプラットフォームにすることができます。

興味があれば、facebookが提供するこの実装の猶予を見ることができます

9.2UNIXドメインソケット+ cmsg

別の考え方は、unixドメインソケット+ cmsgを介して渡すことです。親プロセスが開始しても、ForkExecを使用して子プロセスを作成しますが、listenfdをProcAttrに渡しません。

子プロセスを作成する前に、親プロセスはunixドメインソケットを作成してリッスンします。子プロセスが開始されると、このunixドメインソケットへの接続が確立されます。親プロセスは、cmsgを介してlistenfdを子プロセスに送信し始めます。fdを取得する方法は同じです。 9.1と同じように、fdシャットダウンの問題も同じ方法で処理する必要があります。

子プロセスはunixドメインソケットに接続し、cmsgの受信を開始します。カーネルが子プロセスがメッセージを受信するのを支援すると、親プロセスのfdがあることがわかります。カーネルは、対応するファイルの説明を見つけ、子プロセスにfdを割り当てて、2つの間のマッピングを確立します。関係。次に、子プロセスに戻ると、子プロセスはファイルの説明に対応するfdを取得します。os.NewFile(fd)を介してファイルを取得でき、次にnet.FileListenerまたはnet.PacketConnを介してtcplistenerまたはudpconnを取得できます。

リスニングアドレスを取得して論理サービスを関連付ける残りのアクションは、9.1の要約で説明したものと同じです。

ここでは、誰もが理解してテストできるように、実行可能な簡略化されたバージョンのデモも提供します。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"

 passfd "github.com/ftrvxmtrx/fd"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"
const unixsockname = "/tmp/xxxxxxxxxxxxxxxxx.sock"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

  ln, err := net.Listen("tcp", "localhost:8888")
  if err != nil {
   panic(err)
  }

  wg := sync.WaitGroup{}
  wg.Add(1)
  go func() {
   defer wg.Done()
   for {
    ln.Accept()
   }
  }()

  tcpln := ln.(*net.TCPListener)
  f, err := tcpln.File()
  if err != nil {
   panic(err)
  }

  os.Setenv(envRestart, "1")
  os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

  _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
   Env:   os.Environ(),
   Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), /*f.Fd()*/}, // comment this when test unixsock
   Sys:   nil,
  })
  if err != nil {
   panic(err)
  }
  log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())

  os.Remove(unixsockname)
  unix, err := net.Listen("unix", unixsockname)
  if err != nil {
   panic(err)
  }
  unixconn, err := unix.Accept()
  if err != nil {
   panic(err)
  }
  err = passfd.Put(unixconn.(*net.UnixConn), f)
  if err != nil {
   panic(err)
  }

  f.Close()
  wg.Wait()

 } else {

  v := os.Getenv(envListenFD)
  fd, err := strconv.ParseInt(v, 10, 64)
  if err != nil {
   panic(err)
  }
  log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

  // case1: 有同学认为以通过环境变量传fd,通过环境变量肯定是不行的,fd根本不对应子进程中的fd
  //ff := os.NewFile(uintptr(fd), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  log.Println(err)
  // }
  //}

  // case2: 如果只有一个listenfd的情况下,那如果fork子进程时保证只传0\1\2\listenfd,那子进程中listenfd一定是3
  //ff := os.NewFile(uintptr(3), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  panic(err)
  // }
  // // pause, ctrl+d to continue
  // ioutil.ReadAll(os.Stdin)
  // fmt.Println("....")
  // _, err = net.FileListener(ff) //会dup一个fd出来,有多个listener
  // if err != nil {
  //  panic(err)
  // }
  // // lsof -P -p $pid, 会发现有两个listenfd
  // time.Sleep(time.Minute)
  //}
  // 这里我们暂停下,方便运行系统命令来查看进程当前的一些状态
  // run: lsof -P -p $pid,检查下listenfd情况

  ioutil.ReadAll(os.Stdin)
  fmt.Println(".....")

  unixconn, err := net.Dial("unix", unixsockname)
  if err != nil {
   panic(err)
  }

  files, err := passfd.Get(unixconn.(*net.UnixConn), 1, nil)
  if err != nil {
   panic(err)
  }

  // 这里再运行命令:lsof -P -p $pid再检查下listenfd情况

  f := files[0]
  f.Stat()

  time.Sleep(time.Minute)
 }
}

この実装は、unixのようなシステムに限定されています。

混合サービスの状況がある場合は、同じ名前による問題を回避するために使用されるunixドメインソケットのファイル名を考慮する必要があります。unixドメインソケットの名前として「processname.pid」を使用することを検討し、環境変数を使用して変更することができます。子プロセスに渡されます。

10.ホットリスタートを実現するために移動します:listenfdを介してリスナーを再構築する方法

前述のように、fdを取得しても、それがtcpリスナーに対応するのかudpconnに対応するのかはまだわかりません。どうすればよいですか?すべて試してみてください。

file, err := os.NewFile(fd)
// check error

tcpln, err := net.FileListener(file)
// check error

udpconn, err := net.PacketConn(file)
// check error

11.ホットリスタートを実現するために移動します。親プロセスはスムーズに終了します

親プロセスはどのようにスムーズに終了しますか?これは、親プロセスのロジックがスムーズに停止するかどうかによって異なります。

11.1。確立された接続での要求の処理

次の2つの側面から始めることができます。

  • 読み取りをシャットダウンし、新しい要求を受け入れなくなります。ピアは、データの書き込みを続行すると障害を認識します。

  • 接続で正常に受信された要求の処理を続行します。処理が完了したら、パケットを返して接続を閉じます。

また、リーダーを閉じるのではなく、接続がアイドル状態になるまでしばらく待ってから閉じることも考えられます。できるだけ早く閉じるかどうかは、要件に沿っているかどうか、シーンや要件と組み合わせる必要があります。

可用性要件がより厳しい場合は、connfdおよびconnfdの読み取りおよび書き込みバッファーデータを処理のために子プロセスに渡すことも検討する必要があります。

11.2。メッセージサービス

  • サービスのメッセージ消費と確認メカニズムが妥当かどうかを確認します

  • これ以上の新しいメッセージはありません

  • 受信したメッセージを処理した後に終了します

11.3.AtExitクリーンアップタスクをカスタマイズする

一部のタスクにはいくつかのカスタムタスクがあります。終了する前にプロセスを実行できることを願っています。これにより、AtExitと同様の登録機能が提供され、プロセスが終了する前にビジネス定義のクリーンアップロジックを実行できるようになります。

スムーズな再起動であろうと他の通常の終了であろうと、このサポートには一定の需要があります。

12.その他

シナリオによっては、connfdの対応する読み取りおよび書き込みデータを含め、connfdを転送することも望ましい場合があります。

たとえば、接続の再利用のシナリオでは、クライアントは同じ接続を介して複数の要求を送信する場合があります。サーバーが途中でホットリスタート操作を実行した場合、サーバーが直接読み取りに接続して閉じると、その後のクライアントのデータ送信は失敗します。最後に接続が閉じられると、以前に受信した要求が正常に応答しない場合があります。この場合、サーバーが接続要求の処理を続行し、接続がアイドル状態になるまで待ってから閉じると見なすことができます。常にアイドル状態になりますか?可能。

実際、サーバーはクライアントが接続多重化モードを採用するかどうかを予測できません。より信頼性の高い処理方法を選択することをお勧めします。シーンの要件がより厳しく、上位層で再試行して解決したくない場合。これは、connfdとconnfdで読み書きされたバッファデータを子プロセスに渡し、子プロセスに渡して処理することと考えられます。現時点では、注意すべき点が多く、処理が複雑です。興味のある方は、mosnの実装を参照してください。 。

13.まとめ

サービスのスムーズな再起動とアップグレードを確実にする方法として、ホットリスタートは今日でも非常に価値があります。この記事では、ホットリスタートを実装するためのいくつかの一般的なアイデアについて説明し、デモを通じてgoサービスでホットリスタートを実装する方法について説明します。すべての人に完全なホットリスタートの例を提供しているわけではありませんが、読んだ後で自分で実装できるはずだと思います。

作者のレベルが限られているため、説明の省略は避けられません。訂正してください。

参考記事

  1. Unix Advanced Programming:Interprocess Communication、Steven Richards

  2. mosn起動プロセス:https://mosn.io/blog/code/mosn-startup/

おすすめ

転載: blog.csdn.net/Tencent_TEG/article/details/108505187