Go は RabbitMQ を使用してメッセージ非同期通信の練習問題を完成させ、レコードを解決します

序文

背景: ブロックチェーン プロジェクトの要件により、同時実行性の高いビッグ データ環境では、同時にチェーン上のデータ結果をリアルタイムで同期してビジネス バックエンド (Java によって実装されたビジネス バックエンド) に返すことができないため、非同期通信ミドルウェアの利用を検討し、要件を満たしたrabbitMQを自社で構築し、APIリクエストを返す方式に置き換えるために、プロジェクトの技術的な変革を行います。異言語コミュニケーション、メッセージミドルウェア、モード選択、メッセージ本文受信変換共同デバッグなどの一連の問題が含まれており、これが唯一の記録です。
要件:
ビジネス バックエンド (Java) はチェーン上の情報構造を提供します [プロデューサー – メッセージ送信] –> go のバックエンドに移動してブロックチェーン チェーン メソッドを呼び出します [コンシューマー – メッセージを消費] –> チェーン スマート コントラクトを呼び出して返します結果 その後、さまざまな状況に応じてアップリンクの結果を返します [プロデューサー – Java バックエンドにメッセージを送信]

テクノロジーの選択

  • メッセージミドルウェア: RabbitMQ (詳細なし)
    モード選択: ルーティング ルーティングモード (バージョンの変更と調整、後続の落とし穴の原因の 1 つ)
    スイッチタイプ: ダイレクト (直接接続)
    メッセージの信頼性を確保: 手動確認 ACK メソッドを選択 (別の方法)落とし穴)
  • ビジネスバックエンド: Java
  • オンチェーンのビジネス処理: go
  • ブロックチェーン最下層: fisco-bcos

ラビットMQ

RabbitMQ は、Erlang プログラミング言語を使用して Advanced Message Queuing Protocol AMQP (Advanced Message Queuing Protocol) を実装するオープン ソースのメッセージ ブローカー ソフトウェア (メッセージ キュー ミドルウェア) です。メッセージ キュー ミドルウェアの役割: デカップリング、ピーク カット、非同期処理、キャッシュ ストレージ、メッセージ通信、システムの拡張性を向上させます。

RabbitMQ の機能

  • 信頼性: メッセージ配信の信頼性を確保するための永続性、送信確認などのメカニズムを通じて
  • スケーラビリティ: 複数の RabbitMQ ノードでクラスターを形成可能
  • 高可用性: キューを RabbitMQ クラスター内でミラーリングできるため、一部のノードがハングアップした場合でもキューを引き続き使用できます。
  • 複数のプロトコル: AMQP をネイティブにサポートし、STOMP、MQTT、その他のプロトコルもサポートします
  • リッチ クライアント: 一般的に使用されるすべてのプログラミング言語が RabbitMQ をサポートしています
  • 管理インターフェース:WEB管理インターフェースを搭載
  • プラグインのメカニズム: RabbitMQ は、必要に応じて拡張できる多くのプラグインを提供します。

問題とその解決策

質問 1. 履歴メッセージはルーティング モードでは使用できません

プロジェクトの要件により、ルーティング ルーティング モードを選択しましたが、テスト中に、go バックエンドが履歴キュー内の履歴データ (つまり、消費コルーチンの実行時に Java バックエンドによって開始されたメッセージ) を取得できないことがわかりました。は有効になっていません)、取得できるのはコンシューマ コルーチンの開始後、新しく追加されたメッセージ本文をキューに追加することだけです。

質問 2: ルーティング モードで手動確認が失敗する

前の質問の派生で、消費コルーチンが有効になった後に取得できるメッセージ本文では、自動 ACK 確認が選択されている場合、消費は依然として成功した消費を確認せず、手動 ACK は依然として失敗します。これは問題です。

質問1、質問2 エラーの原因

最終調査の理由は、ルーティング モードへのわずかな変更によって引き起こされた後続の連鎖反応であることが判明しました。
公式 Web サイトやその他の参考ブログ投稿では、ルーティング モードのプロデューサーは、mq インスタンスの作成時にスイッチ名 ExchangeName と routingkey を指定するだけで済みます。この時点で、メッセージは一時キューに配置されます。必要に応じてすぐに作成され、使用されず、自動的に削除されます。一時キューの宣言は非常に簡単で、キューを宣言するときにキュー名を空の文字列として記述するだけで、一時キューには amq.gen-JzTY20BRgKO-HjmUJj0wLg などのランダムな名前が割り当てられます。接続が閉じられると、一時キューは削除されます。
ここに画像の説明を挿入

var (
	mqURL = viper.GetString("RabbitMQ.Url") //连接信息读取配置文件 amqp:// 账号 密码@地址:端口号/vhost
)

//RabbitMQ结构体
type RabbitMQ struct {
	//连接
	conn    *amqp.Connection
	channel *amqp.Channel
	//队列
	QueueName string
	//交换机名称
	ExChange string
	//绑定的key名称
	Key string
	//连接的信息,上面已经定义好了
	MqUrl string
}
//rabbitmq的路由模式。
//主要特点不仅一个消息可以被多个消费者消费还可以由生产端指定消费者。
//这里相对比订阅模式就多了一个routingkey的设计,也是通过这个来指定消费者的。
//创建exchange的kind需要是"direct",不然就不是roting模式了。
//创建rabbitmq实例,这里有了routingkey为参数了。
func NewRabbitMqRouting(exchangeName string, routingKey string) *RabbitMQ {
	return NewRabbitMQ("", exchangeName, routingKey)
}

//创建RabbitMQ结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
	rabbitMQ := &RabbitMQ{QueueName: queueName, ExChange: exchange, Key: key, MqUrl: mqURL}
	var err error
	//创建rabbitMQ连接
	rabbitMQ.conn, err = amqp.Dial(rabbitMQ.MqUrl)
	rabbitMQ.failOnErr(err, "创建rabbit的路由实例的时候连接出现问题!")
	rabbitMQ.channel, err = rabbitMQ.conn.Channel()
	rabbitMQ.failOnErr(err, "创建rabbitmq的路由实例时获取channel出错")
	return rabbitMQ
}
//错误处理函数
func (r *RabbitMQ) failOnErr(err error, message string) {
	if err != nil {
		log.Fatalf("%s:%s", message, err)
		panic(fmt.Sprintf("%s:%s", message, err))
	}
}
//断开channel和connection
func (r *RabbitMQ) Destory() {
	r.channel.Close()
	r.conn.Close()
}

ただし、その後の問題の特定と、メッセージ ミドルウェアに関する多くの Java ブログ投稿を容易にするために、Java バックエンド開発の同僚は、指定されたキュー名を作成し、それをスイッチにバインドすることを選択しました。プロデューサーがメッセージを送信するのには問題ありませんが、メッセージを受信して​​消費すると、問題 1 と問題 2 で説明されている状況が発生します。

解決

コンシューマのメソッドは公式のものとは少し異なり、スイッチの作成、キューの作成、キューとスイッチのバインドのためのコードをコメントアウトし、mq インスタンスのチャネルから直接消費情報を取得します (前提条件: 事前にキューを作成する必要があります)。実行、スイッチ、バインディング スイッチ、およびキューの関係は、コントロール パネルまたはコードで作成できます。つまり、最初に存在する必要があります)

//路由模式接收信息-测试
func (r *RabbitMQ) ReceiveRouting() {
	尝试创建交换机,不存在创建
	//err := r.channel.ExchangeDeclare(
	//	//交换机名称
	//	r.ExChange,
	//	//交换机类型 广播类型
	//	"direct",
	//	//是否持久化
	//	true,
	//	//是否字段删除
	//	false,
	//	//true表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
	//	false,
	//	//是否阻塞 true表示要等待服务器的响应
	//	false,
	//	nil,
	//)
	//r.failOnErr(err, "failed to declare an excha"+"nge")
	2试探性创建队列,创建队列
	//q, err := r.channel.QueueDeclare(
	//	"", //随机生产队列名称
	//	false,
	//	false,
	//	true,
	//	false,
	//	nil,
	//)
	//r.failOnErr(err, "Failed to declare a queue")
	绑定队列到exchange中
	//err = r.channel.QueueBind(
	//	q.Name,
	//	//在pub/sub模式下,这里的key要为空
	//	r.Key,
	//	r.ExChange,
	//	false,
	//	nil,
	//)
	//消费消息
	message, err := r.channel.Consume(
		"queue_data_chain_reply",
		"",
		false, // 是否自动确认消费
		false,
		false,
		false,
		nil,
	)
	forever := make(chan bool)
	go func() {
		for d := range message {
			//chainDataRepBody := new(model.ChainDataRep)
			//err = json.Unmarshal(d.Body, &chainDataRepBody)
			d.Ack(true) // 更改为手动确认ack
			result := make(map[string]interface{})
			err = json.Unmarshal(d.Body, &result)
			if err != nil {
				fmt.Println(err)
			}
			for k, v := range result {
				fmt.Println(k)
				fmt.Println(v)
			}
			//fmt.Printf("消费的id是%s", chainDataRepBody.ID)
			fmt.Printf("消费的数据是%s", d.Body)
			//log.Printf("消费消息:%s,", d.Body)
		}
	}()
	fmt.Println("退出请按 Ctrl+C")
	<-forever
}

上記のテストコードと同様に、消費できない履歴を解決し、手動確認ACKを保存します。

質問 3. RabbitMQ の手動 ACK 確認を変更する方法

Q: メッセージ確認の問題。コンシューマーがメッセージを処理していて、メッセージが処理される前にメッセージが突然中断され (おそらくコンシューマーのプロセスが強制終了される)、この時点でコンシューマーが処理中のメッセージが失われます。このメッセージ損失の問題を解決するにはどうすればよいでしょうか?

解決

回答: 現時点ではプロデューサーとは関係ありませんが、主にコンシューマー側の設定です。コンシューマ側のコードの Consume メソッドの下にある autoAck 自動確認フィールドは false に設定されており、自動確認を実行しないことを示しています。代わりに、メッセージが処理された後に手動で確認します。そのため、メッセージが処理される前に中断され、RabbitMQ サーバーがメッセージの確認を受信しない場合、キュー内のメッセージは再び再配布されます。コンシューマ側は次のように変更します(コードは上記と同じです)。
ここに画像の説明を挿入

質問4. java\goで送信情報の種類を変換する

go の amqp メッセージ本文は []byte 型であるため、オブジェクトまたは構造体を go とその dataBytes の間で転送する必要があります。err := json.Marshal(data) をバイト型ストレージに転送し、.Unmarshal( を受信した後に json を使用します。 ) 逆シリアル化と解析後に使用できます。

result := make(map[string]interface{})
err = json.Unmarshal(d.Body, &result)

ただし、私が使用している Go 言語は Java バックエンドと通信する必要があり、Java バイトの範囲は -128 ~ 127 ですが、golang バイトは uint8 のエイリアスであり、範囲は 0 ~ 255 であるため、型の不一致の問題が発生します。会話

解決

Java プロデューサーが情報を送信 –> 消費者が消費する 消費する

Java バックエンドがプロデューサーとして機能する場合、送信されたメッセージは json 型でメッセージ キューに入れられ、go から取得されたメッセージ本文 json.Unmarshal() は指定されたオブジェクト構造に変換できます。

Go プロデューサーがメッセージを送信 –> Java コンシューマーがメッセージを消費

go がプロデューサーの場合、オブジェクト json.Marshal() をシリアル化するメッセージを送信しますが、amqp.Publishing メッセージのコンテンツ タイプを application/json に送信するにはルーティング モードを変更する必要があります (公式のケースや他のブログ投稿は次のとおりです)。テキスト/プレーンタイプ)

メッセージを送信するルーティング モード

//路由模式发送信息
func (r *RabbitMQ) PublishRouting(message []byte) {
	//第一步,尝试创建交换机,与pub/sub模式不同的是这里的kind需要是direct
	err := r.channel.ExchangeDeclare(
		//交换机名称
		r.ExChange,
		//交换机类型 广播类型
		"direct",
		//是否持久化
		true,
		//是否字段删除
		false,
		//true表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
		false,
		//是否阻塞 true表示要等待服务器的响应
		false,
		nil,
	)
	r.failOnErr(err, "路由模式,尝试创建交换机失败")
	//发送信息
	err = r.channel.Publish(
		r.ExChange,
		//要设置
		r.Key,
		false,
		false,
		amqp.Publishing{
			//类型
			//ContentType: "text/plain", // byte类型
			ContentType: "application/json", // 更改为json类型
			//消息
			Body: message,
		})
}

質問 5. コルーチン消費メッセージの論理コードで、MQ へのメッセージの送信が失敗します。

ビジネスは、キューのメッセージを処理するためにコルーチンを開始する必要があるため、処理結果を別のキューに送信する必要があります (つまり、MQ コンシューマのビジネス ロジックを処理する際に、別の MQ インスタンスのプロデューサーとしてメッセージを送信します)。消費時は手動 ACK モードなので、以前の処理シーケンスは次のとおりです。

  1. コルーチンのコンシューマーとしてメッセージ本文を受信します。
  2. ビジネスロジックを処理する
  3. 戻り構造をカプセル化する
  4. プロデューサーとして、返信メッセージを別のキューに送信します
  5. 手動で ack を実行すると、メッセージが確認されます
    。この時点で問題が発生しました。メッセージは消費されたことが確認されましたが、メッセージは別のキューに送信されていませんでした。

解決

ステップ 4 と 5 のコードを置き換えて、最初に手動で確認して消費を確認し、次に消費を送信して問題を解決します。この論理はわかりませんが、問題の記録としてのみ使用されます。コミュニケーションを歓迎します。

質問 6. 受信側の MQ メッセージ本文の一部のフィールドが欠落しています

低レベルのエラーですが、Java バックエンドとのオブジェクト構造のやり取りに int 型のフィールドが 2 つあることを思い出させます。これらはチェーンの状態判断として使用されますが、go がプロデューサーとしてキューに送信された後、 Java バックエンド コンシューマとして受信すると、2 つの int 型フィールドが欠落しており、キーも値も存在しないことがわかります。

解決

これも int 型送信時の言語の不一致によるおかしな問題だと思っていたのですが、後になって構造体中の json コマンドを変更するのを忘れていたことが分かりました(例えば下図)。これは、mq キューにありません。これは低レベルの間違いですが、混乱しているとこのような低レベルの間違いを犯しやすく、それでも気になるので、自分に思い出させるために記録してください。
ここに画像の説明を挿入

質問 7. 圧力テストによりメッセージ チャネルがブロックされ、無限ループが発生し、メッセージ本文が失われ、手動 ACK が失敗します。

当初、上記の問題を解決すれば基本的にビジネス要件を満たすことができると考えていましたが、tps2000 強度を使用した場合に、重要なビジネス メッセージをキューに入れる - 消費 - このロジックに戻るというストレス テストを行ったところ、プレッシャー テストに合格できませんでした。ストレステスト、消費が発生しました 情報が約 1,000 件ある場合、メッセージ本文のない情報を取得しましたが、それでも手動 ACK が失敗し、チャネルがブロックされ、無限ループが発生しました。

解決策: qos パラメータのプリフェッチを設定して、メッセージの輻輳の問題を解決します。

// 启用QoS,每次预取5条消息,避免消息处理不过来,全部堆积在本地缓存里
r.channel.Qos(
		//每次队列只消费一个消息 这个消息处理不完服务器不会发送第二个消息过来
		//当前消费者一次能接受的最大消息数量-自定义设置
		1,
		//服务器传递的最大容量
		0,
		//如果为true 对channel可用 false则只对当前队列可用
		false,
	)

QoS をオンにします。RabbitMQ キューが 5 つの未確認メッセージに達すると、それ以上のメッセージはコンシューマにプッシュされません。解決策はタイムリーではありません。ACK プリフェッチとは、単一のコンシューマが消費できる未確認メッセージの最大数を指します

q は各コンシューマのバッファを設定します。サイズはプリフェッチです。メッセージを受信するたびに、MQ はメッセージをバッファにプッシュしてからクライアントにプッシュします。ack メッセージを受信すると (コンシューマーがbaseack コマンドを発行すると)、mq はバッファーから位置を空けてから、新しいメッセージを追加します。ただし、この時点でバッファがいっぱいの場合、MQ はブロック状態になります。
より具体的に説明すると、プリフェッチ値が 10 に設定され、コンシューマが 2 つあるとします。つまり、各コンシューマは毎回キューから 10 個のメッセージをプリフェッチし、消費するためにローカルにキャッシュします。同時に、未確認のチャネル数は 20 になります。Rabbit の配信順序は、最初に 10 個のメッセージを Consumer1 に配信し、次に 10 個のメッセージを Consumer2 に配信することです。この時点で配信される新しいメッセージがある場合、最初にチャネルの未確認数が 20 に等しいかどうかを判断します。20 に等しい場合、メッセージはコンシューマに配信されず、メッセージはキューに残り続けます。 。その後、コンシューマはメッセージに対して ack を行いますが、このときの unacked は 19 であり、Rabbit はどのコンシューマの unacked が 10 未満であるかを判断し、どのコンシューマに配信します。

手動 ACK を設定し、QOS と連携すると、問題は解決され、圧力テストによって同様の問題が再び発生することはなくなります。

優れたホイール

上記の問題をすべて解決した後、クライアントから大量のデータを同時にチェーンにアップロードすると、複雑な処理により mq の消費速度が理想的ではないため、半分の時間がかかることがわかりました。 2000 メッセージの消費を完了するまでに 1 時間から 1 時間かかります。他に異常はありません。間違っていますが、それでもパフォーマンスが低すぎるため、アプリケーションのエクスペリエンスが必然的に十分ではないため、検索した結果、優れたホイールを見つけました。上司はそれを非常にうまくパッケージ化しており、接続プールチャネルの再利用、エラーの再試行、永続キュー、クラスターポーリングなどの他の側面のデッド処理を考慮しています。上記は確かに、公式 Web サイトの事例と Rabbitmq に関する限られた Go データに基づいて統合した利用可能なバージョンであると言わざるを得ませんが、パフォーマンスは十分ではありません。大手のコードを参照した後、多くのことを学びました。波を記録して共有します。誰にとっても良いホイールです。
tym_hmm / Rabbitmq-pool-go
コードをローカルにプルし、プロジェクトの要件に従って調整しました。独自の圧力テストを経て、パフォーマンスが n レベルに直接向上しました。上司もこう書いていました。実稼働環境に適用されます. 5200W が qbs 3000 をリクエストすると、接続プールは圧力がないことを示し、クラスターのデプロイも完了しています。個人テストは使いやすいです。(追記:使用する前提として、公式の基本的な書き方や一部のパラメータの意味も理解しておく必要があります)

参考記事

いくつかのモード例を含むいくつかの記事の参照は非常に詳細です
go 操作 RabbitMQ
RabbitMQ 公式 Web サイト ルーティング モード go language
[go-zero] go-zero と amqp go メッセージ プッシュを実現するために Rabbitmq を統合します go メッセージ キュー (ベスト プラクティス) golang は
RabbitMQ メッセージ キューを実装します
RabbitMQ シリーズの記事
amqp
RabbitMQ は
Golang のメッセージ キューを使用します - RabbitMQ は
RabbitMQ を使用します FAQ
RabbitMq QOS プリフェッチ メッセージの輻輳問題

おすすめ

転載: blog.csdn.net/ic_xcc/article/details/125679592