2025,江端さんの技術メモ

Go言語を使ったマルチエージェントシミュレーションが、他の言語よりもメリットがある点とデメリットがある点について論じて下さい

一つ一つが、大変納得した内容だったので、私のメモ用に記録しておきます。

 

Go言語を用いたマルチエージェントシミュレーション(MAS)の利点と欠点について、他の言語(C++, Python, Juliaなど)と比較しながら論じる。

1. Go言語を使うメリット

(1) 並行処理の容易さ

Goはgoroutineを用いた並行処理が特徴であり、エージェントごとにgoroutineを割り当てることで効率的な並列処理が可能である。他の言語ではスレッド管理(C++のstd::threadやPythonのthreading)が必要になるが、Goではシンプルな記述で実装できる。

例:

func agent(id int) {
    for {
        fmt.Printf("Agent %d is moving...\n", id)
        time.Sleep(time.Millisecond * 500)
    }
}

func main() {
    for i := 0; i < 10; i++ {
        go agent(i) // 各エージェントをgoroutineで実行
    }
    select {} // メインスレッドを終了させない
}

このようにGoの並行処理はシンプルかつ軽量であり、大量のエージェントを扱うシミュレーションに適している。


(2) シンプルなコードとメンテナンス性

Goはシンプルな文法強力な標準ライブラリを持つため、コードの可読性が高く、開発者間での共有が容易である。
C++ではテンプレートやマルチスレッド処理が複雑になりやすく、PythonではGIL(Global Interpreter Lock)の影響で並列処理が制限される。一方、GoはGILの問題がなく、コードの記述量も比較的少ないため、長期的な開発に向いている。


(3) 高速なコンパイルと実行

Goはコンパイルが非常に速く、バイナリを直接実行できるため、デバッグやテストのサイクルを短縮できる。C++のように長時間のコンパイルが不要で、Pythonのようなインタープリタ型の遅延もない。


(4) クロスプラットフォームの容易さ

Goは静的バイナリを生成できるため、Windows, Linux, macOS などの異なる環境での実行が容易である。C++ではコンパイル時にライブラリ依存の問題が生じやすく、Pythonでは環境設定(仮想環境やパッケージ管理)が面倒になりがちだが、Goでは1つのバイナリで解決できる。


(5) クラウドやWebとの親和性

GoはもともとGoogleが開発した言語であり、クラウド環境(GCP, AWS)やWebとの親和性が高い。
シミュレーション結果をWeb API経由でリアルタイム表示したり、分散処理の一部をクラウド上で実行する際に、Goの標準ライブラリだけでHTTPサーバを簡単に実装できるのは大きなメリット。


2. Go言語を使うデメリット

(1) 高速計算ライブラリが少ない

PythonやC++には、最適化された数値計算ライブラリ(NumPy, SciPy, Eigen, OpenMPなど)が豊富にあるのに対し、Goは数値計算や線形代数のライブラリが少ない
そのため、大量の行列演算や物理シミュレーションを行う場合、Go単体では計算効率が劣る可能性がある。

対策:

  • Goで数値計算を行う場合は、gonum ライブラリを使用する。
  • 必要に応じて、GoからC/C++(BLAS, LAPACK)を呼び出す。

(2) GPUサポートが弱い

C++はCUDAやOpenCLを利用してGPUによる並列計算が可能であり、PythonもTensorFlowやCuPyを通じてGPUを活用できる。しかし、Goは公式にはGPUを直接サポートしていないため、CUDAを使う場合はCGO経由でC++のライブラリを呼び出す必要がある

対策:

  • gorgonia(Go向けのニューラルネットワークライブラリ)を利用すると、一部のGPU計算が可能。
  • CUDA/OpenCLを直接利用したい場合は、GoからC++のラッパーを呼び出す。

(3) 研究事例が少ない

PythonやC++は、数多くの学術論文や研究事例があり、豊富なサンプルコードが存在する。一方で、Goは学術分野ではあまり使われておらず、論文や研究事例が少ない
特に、MAS関連のライブラリやフレームワークが未成熟なため、開発者が独自に実装する必要がある。

対策:

  • MASの一般的なアルゴリズム(Boidモデル、Cellular Automatonなど)をGoで実装する。
  • 他言語のライブラリと組み合わせて使用する。

(4) ガベージコレクションの影響

Goのガベージコレクション(GC)は自動的にメモリを管理するが、大規模なシミュレーションではGCの影響で予期しないタイミングで一時的な遅延が発生する可能性がある。
C++のように明示的なメモリ管理ができないため、リアルタイム性を求めるシミュレーションでは不利になることがある。

対策:

  • メモリ割り当てを最小限に抑える設計を行う(オブジェクトの再利用)。
  • sync.Pool を活用してメモリ管理のオーバーヘッドを減らす。

3. 結論

Go言語を使ったMASは、並行処理の容易さ・コードのシンプルさ・クラウドとの親和性に優れており、大規模エージェントシミュレーションの実装がしやすい
しかし、GPUサポートの弱さ・計算ライブラリの不足・研究事例の少なさといった課題もあり、特に高度な数値計算が必要なシミュレーションではPythonやC++の方が適している

Go言語が適しているケース:

  • 大規模なエージェント数をgoroutineで管理
  • ネットワーク・クラウド連携が必要
  • シンプルな設計を重視

PythonやC++の方が適しているケース:

  • 高度な数値計算(線形代数、最適化)
  • GPUを活用したシミュレーション
  • 既存のライブラリや研究事例を活用

最適なアプローチ:
Go単体でMASを実装するよりも、計算部分をPython/C++で処理し、Goは並列制御やネットワーク管理に使うというハイブリッドアプローチが有効となる。


昨日、

PrumeMobileを使った人流の見える化

について記載したんだけど、これ簡単に言うと『goroutineを止めて、サブルーチンにした』というものです。

300万のエージェントが、メインルーチンの情報変更に上手く連動しなくなると、"goroutineの自己破壊命令が届かない"という問題がボロボロでてきたからです。

まあ、それでも、『数十万人分、361万行の移動データを、全部表示して正常終了した』というGo言語の凄さには、相変らず感心しているんですけどね(昨夜、PC回しながら寝たのですが、今朝、全部のオブジェクトが消えて、正常終了していました)。

2025,江端さんの技術メモ

起動方法
$go run server22-1.go

http://localhost:8080

$go run pm_proxy_single_socket.go

 

// server22-1.go

/*
このプログラムは、WebSocket を利用したリアルタイム位置情報サーバーである。
- クライアントから送信された位置情報を受信し、処理後にレスポンスを返す。
- 位置情報はチャネルを介して他のクライアントと共有される。
- `sync.Mutex` を使用してスレッド安全な処理を実現。
- WebSocket の接続エラー時には適切にログを出力し、安定した通信を確保。
- HTML を提供し、ブラウザ上で WebSocket 通信を確認可能。
*/
package main

import (
	"flag"
	"html/template"
	"log"
	"net/http"
	"sync"

	"github.com/gorilla/websocket"
)

// GetLoc GetLoc
type GetLoc struct {
	ID  int     `json:"id"`
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
	//Address string  `json:"address"`
}

// var addr = flag.String("addr", "localhost:8080", "http service address")
var addr = flag.String("addr", "0.0.0.0:8080", "http service address") // テスト

var upgrader = websocket.Upgrader{} // use default options

var chan2_1 = make(chan GetLoc)

var maxid = 0

var mutex sync.Mutex

func echo2(w http.ResponseWriter, r *http.Request) {
	c, err := upgrader.Upgrade(w, r, nil) // cはサーバのコネクション
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	defer c.Close()

	//mutex := new(sync.Mutex)

	for {
		//mt, message, err := c.ReadMessage() // クライアントからのメッセージの受信(mtはクライアント識別子)
		//_, _, err := c.ReadMessage() // クライアントからのメッセージの受信(mtはクライアント識別子)

		//mutex.Lock()  // ここに置くとデッドロックしてしまう

		gl := new(GetLoc)

		err := c.ReadJSON(&gl) // クライアントからのメッセージの受信

		mutex.Lock()

		// 原因不明の対処処理
		if gl.ID == 0 && gl.Lat < 0.01 && gl.Lng < 0.01 {
			mutex.Unlock()
			break
		} else if gl.ID < -1 { // 受理できないメッセージとして返信する
			//条件分岐 (変なIDが付与されているメッセージは潰す)
			//if (gl.ID > maxid) || (gl.ID < -1) { // 受理できないメッセージとして返信する

			gl.ID = -1
			gl.Lat = -999
			gl.Lng = -999
			err2 := c.WriteJSON(gl)
			if err2 != nil {
				log.Println("write1:", err2)
				mutex.Unlock()
				break
			}
		} else { // それ以外は転送する
			/*
				log.Printf("echo2 after c.WriteJSON(gl) ID:%d", gl.ID)
				log.Printf("echo2 after c.WriteJSON(gl) Lat:%f", gl.Lat)
				log.Printf("echo2 after c.WriteJSON(gl) Lng:%f", gl.Lng)
			*/
			if err != nil {
				log.Println("read:", err)
				mutex.Unlock()
				break
			}
			//fmt.Printf("echo2 before chan2_1 <- *gl\n")
			chan2_1 <- *gl
			//fmt.Printf("echo2 after chan2_1 <- *gl\n")

			//で、ここで受けとる
			//gl2 := new(GetLoc)
			//fmt.Printf("echo2 before gl2 := <-chan2_1\n")
			gl2 := <-chan2_1
			maxid = gl2.ID // ID最大値の更新
			/*
				log.Printf("echo2 after gl2 := <-chan2_1 ID:%d", gl2.ID)
				log.Printf("echo2 after gl2 := <-chan2_1 Lat:%f", gl2.Lat)
				log.Printf("echo2 after gl2 := <-chan2_1 Lng:%f", gl2.Lng)
			*/
			//fmt.Printf("echo2 before err2 := c.WriteJSON(gl2)\n")
			err2 := c.WriteJSON(gl2)
			//fmt.Printf("echo2 after err2 := c.WriteJSON(gl2)\n")
			if err2 != nil {
				log.Println("write2:", err2)
				mutex.Unlock()
				break
			}
			//fmt.Printf("end of echo2\n")

		}

		mutex.Unlock()
	}
}

func echo(w http.ResponseWriter, r *http.Request) {

	c, err := upgrader.Upgrade(w, r, nil) // cはサーバのコネクション
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	defer c.Close()

	/*	ここでロックして待つ */

	for {

		//fmt.Printf("echo before gl := <-chan2_1\n")
		gl := <-chan2_1
		//fmt.Printf("echo after gl := <-chan2_1\n")

		//fmt.Printf("echo before err = c.WriteJSON(gl) gl2.id = %d\n", gl.ID)
		//fmt.Printf("echo before err = c.WriteJSON(gl) gl2.lat = %f\n", gl.Lat)
		//fmt.Printf("echo before err = c.WriteJSON(gl) gl2.lng= %f\n", gl.Lng)
		err = c.WriteJSON(gl)
		if err != nil {
			log.Println("WriteJSON1:", err)
		}
		//fmt.Printf("echo after err = c.WriteJSON(gl)\n")

		//fmt.Printf("echo before err = c.RreadJSON(gl)\n")
		gl2 := new(GetLoc)
		err2 := c.ReadJSON(&gl2)
		//fmt.Printf("echo after err = c.ReadJSON(&gl2) gl2.id = %d\n", gl2.ID)
		//fmt.Printf("echo after err = c.ReadJSON(&gl2) gl2.lat = %f\n", gl2.Lat)
		//fmt.Printf("echo after err = c.ReadJSON(&gl2) gl2.lng= %f\n", gl2.Lng)
		if err2 != nil {
			log.Println("ReadJSON:", err2)
		}
		// ここからチャネルで返す
		//fmt.Printf("echo before chan2_1 <- *gl2 gl2.id = %d\n", gl2.ID)
		//fmt.Printf("echo before chan2_1 <- *gl2 gl2.lat = %f\n", gl2.Lat)
		//fmt.Printf("echo before chan2_1 <- *gl2 gl2.lng = %f\n", gl2.Lng)
		chan2_1 <- *gl2
		//fmt.Printf("echo after chan2_1 <- *gl2\n")
		//fmt.Printf("end of echo\n")
	}

}

func home(w http.ResponseWriter, r *http.Request) {
	homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}

func smartphone(w http.ResponseWriter, r *http.Request) {
	smartphoneTemplate.Execute(w, "ws://"+r.Host+"/echo2")
}

func main() {
	flag.Parse()
	log.SetFlags(0)

	http.HandleFunc("/echo2", echo2)           // echo関数を登録 (サーバとして必要)
	http.HandleFunc("/echo", echo)             // echo関数を登録 (サーバとして必要)
	http.HandleFunc("/", home)                 // home関数を登録
	http.HandleFunc("/smartphone", smartphone) // smartphone関数を登録
	log.Fatal(http.ListenAndServe(*addr, nil)) // localhost:8080で起動をセット
}

var smartphoneTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script> 

function obj(id, lat, lng){
	this.id = id;
	this.lat = lat;
	this.lng = lng;
}

function random(min, max){
	return  Math.random()*(max-min) + min;
}

// var personal_id;

var lat = 35.654543;
var lng = 139.795534;  

window.addEventListener("load", function(evt) {
    var output = document.getElementById("output");
    var input = document.getElementById("input");
    var ws;
    var print = function(message) {
        var d = document.createElement("div");
        d.textContent = message;
        output.appendChild(d);
	};

	var personal_id = 0;

	
	///// 起動時のボタン
	// disabled属性を削除
	document.getElementById("open").removeAttribute("disabled");
	document.getElementById("open").style.color = "black";

	// disabled属性を設定 (closeボタンを非活性化)
	document.getElementById("close").setAttribute("disabled", true);
	document.getElementById("close").style.color = "White";	

	// disabled属性を設定 (sendボタンを非活性化)
	document.getElementById("send").setAttribute("disabled", true);
	document.getElementById("send").style.color = "White";	


		
	document.getElementById("open").onclick = function(evt) {
		console.log("document.getElementById open");

		// disabled属性を設定 (openボタンを非活性化)
		document.getElementById("open").setAttribute("disabled", true);
		document.getElementById("open").style.color = "White";		

		// disabled属性を削除
		document.getElementById("send").removeAttribute("disabled");
		document.getElementById("send").style.color = "black";	

		// disabled属性を削除
		document.getElementById("close").removeAttribute("disabled");
		document.getElementById("close").style.color = "black";	

		////////// 削除2
		// ws = new WebSocket("{{.}}");
		////////// 削除2終り


		////////// 削除1
		//var send_obj = new obj(0, 35.654543,139.795534);  // 最初は"0"でエントリ

		//console.log("open:send_obj");	
		//console.log(send_obj.id);	
		//console.log(send_obj.lat);
		//console.log(send_obj.lng);		

		//var json_obj = JSON.stringify(send_obj);
		//ws.send(json_obj);
		/////////// 削除1終り


        if (ws) {
            return false;
        }
		
		
		////////// 追加2
		ws = new WebSocket("{{.}}");
		////////// 追加2終り

		
        ws.onopen = function(evt) {
			print("OPEN");
		
			//ws = new WebSocket("{{.}}");
			
			////////// 追加1			
			var send_obj = new obj(0, 35.654543,139.795534);  // 最初は"0"でエントリ

			console.log("open:send_obj");	
			console.log(send_obj.id);	
			console.log(send_obj.lat);
			console.log(send_obj.lng);		

			var json_obj = JSON.stringify(send_obj);
			ws.send(json_obj);
			/////////// 追加1終り

		}
		
        ws.onclose = function(evt) {

			print("CLOSE");
            ws = null;
        }

		ws.onmessage = function(evt) {  // 受信したメッセージはここに飛んでくる
			print("RESPONSE: " + evt.data);  // jsonメッセージの内容を表示
			// データをJSON形式に変更
			var obj = JSON.parse(evt.data);

			personal_id = obj.id; // IDの取得(何回も取る必要はないが)
			console.log("personal_id");			
			console.log(personal_id);

			
			if ((Math.abs(obj.lat) > 90.0) || (Math.abs(obj.lng) > 180.0)){ // 異常な座標が入った場合は、マーカーを消去する
				console.log("before ws.close()");
				ws.close();
				console.log("after ws.close()");
			}
		}
		
        ws.onerror = function(evt) {
            print("ERROR: " + evt.data);
        }
        return false;
    };
	
	document.getElementById("send").onclick = function(evt) {

		console.log("document.getElementById send");

		// disabled属性を設定 (openボタンを非活性化)
		document.getElementById("open").setAttribute("disabled", true);
		document.getElementById("open").style.color = "White";	
	
		// disabled属性を削除
		document.getElementById("send").removeAttribute("disabled");
		document.getElementById("send").style.color = "black";	

		// disabled属性を削除
		document.getElementById("close").removeAttribute("disabled");
		document.getElementById("close").style.color = "black";	
	
		if (!ws) {
			console.log("return false send");
			return false;			
		}

		lat += random(0.5, -0.5) * 0.00001 * 10 * 5;
		lng += random(0.5, -0.5) * 0.00002 * 10 * 5

		
		//var send_obj = new obj(personal_id, 35.654543,139.795534);  // idでエントリ
		var send_obj = new obj(personal_id, lat, lng);  // idでエントリ

		console.log("send:send_obj");	
		console.log(send_obj.id);	
		console.log(send_obj.lat);
		console.log(send_obj.lng);		

		var json_obj = JSON.stringify(send_obj);
		ws.send(json_obj);		

		/*
        print("SEND: " + input.value);
        ws.send(input.value);
		return false;
		*/

		return false;	
    };

	document.getElementById("close").onclick = function(evt) {
		console.log(" document.getElementById close");

		// disabled属性を削除
		document.getElementById("open").removeAttribute("disabled");
		document.getElementById("open").style.color = "black";

		// disabled属性を設定 (closeボタンを非活性化)
		document.getElementById("close").setAttribute("disabled", true);
		document.getElementById("close").style.color = "White";	

		// disabled属性を設定 (sendボタンを非活性化)
		document.getElementById("send").setAttribute("disabled", true);
		document.getElementById("send").style.color = "White";			


        if (!ws) {
            return false;
		}
	
		var send_obj = new obj(personal_id, 999.9, 999.9);  // 最初は"0"でエントリ

		console.log("close:send_obj");
		console.log(send_obj.id);		
		console.log(send_obj.lat);
		console.log(send_obj.lng);		

		var json_obj = JSON.stringify(send_obj);
		ws.send(json_obj);

        //ws.close();  // これはws.onmessageの方で実施
        return false;
    };
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server, 
"Send" to send a message to the server and "Close" to close the connection. 
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<!-- <p><input id="input" type="text" value="Hello world!"> -->
<button id="send">Send</button>
<button id="close">Close</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))

var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>PruneMobile</title>

	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-beta.2.rc.2/leaflet.css"/>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-beta.2.rc.2/leaflet.js"></script>

	<script src="http://kobore.net/PruneCluster.js"></script>           <!-- これ、いずれローカルホストから取れるように換える -->
	<link rel="stylesheet" href="http://kobore.net/examples.css"/>      <!-- これも、いずれローカルホストから取れるように換える -->

	<!-- goのテンプレートのローカルって、どこになるんだろう? -->

</head>
<body>
<div id="map"></div>

<script>

	ws = new WebSocket("{{.}}"); // websocketの確立

	/*
	var print = function(message) {
		var d = document.createElement("div");
		d.textContent = message;
		output.appendChild(d);
	};
	*/

	// 引数にはミリ秒を指定。(例:5秒の場合は5000)
	function sleep(a){
  		var dt1 = new Date().getTime();
  		var dt2 = new Date().getTime();
  		while (dt2 < dt1 + a){
			dt2 = new Date().getTime();
		}
  		return;
	}

    var map = L.map("map", {
        attributionControl: false,
        zoomControl: false
	}).setView(new L.LatLng(35.36716428833585, 139.62724520774287), 16); // 富岡		
	//}).setView(new L.LatLng(33.58973407765046, 130.41048227121925), 16); // 中州
    //}).setView(new L.LatLng(35.654543, 139.795534), 18); 豊洲
    // }).setView(new L.LatLng(35.598563, 139.475528), 18); 広袴

    L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
        detectRetina: true,
        maxNativeZoom: 18
    }).addTo(map);

    var leafletView = new PruneClusterForLeaflet(1,1);  // (120,20)がデフォルト

	ws.onopen = function (event) {
	}

	var markers = [];

	//var helicopterIcon = L.icon({ iconUrl: 'http://sintef-9012.github.io/PruneCluster/examples/helicopter.png', iconSize: [48, 48] });
    //var airplaneIcon = L.icon({ iconUrl: 'http://sintef-9012.github.io/PruneCluster/examples/airplane.png', iconSize: [48, 48] });

	// 受信すると、勝手にここに飛んでくる
	ws.onmessage = function (event) {
		// データをJSON形式に変更
		var obj = JSON.parse(event.data);

		console.log("233");	
		console.log(obj.id);
		console.log(obj.lat);						
		console.log(obj.lng);	

		if (obj.id == 0){  // idが未登録の場合
			console.log("obj.id == 0")
			// データをマーカーとして登録
			var marker = new PruneCluster.Marker(obj.lat, obj.lng);

			// 参考資料  http://embed.plnkr.co/WmtpkEqSDJFuFeuiYP54/
			//var marker = new PruneCluster.Marker(obj.lat, obj.lng, {
			//	//popup: "Bell 206 " + i,
			//	icon: helicopterIcon
			//});


			console.log(marker.hashCode);		
			markers.push(marker);
	
			leafletView.RegisterMarker(marker);
	
			console.log(markers);
			console.log(markers.length)

			obj.id = marker.hashCode;
			//ws.send(marker.hashCode); // テキスト送信
			var json_obj = JSON.stringify(obj);
			ws.send(json_obj);			
		} else if ((Math.abs(obj.lat) > 90.0) || (Math.abs(obj.lng) > 180.0)){ // 異常な座標が入った場合は、マーカーを消去する
			console.log("Math.abs(obj.lat) > 180.0)")
			for (let i = 0; i < markers.length; ++i) {
				if (obj.id == markers[i].hashCode){
					console.log(i)
					console.log(obj.id)										
					console.log("obj.id == markers[i].hashCode")

					//leafletView.RemoveMarkers(markers[obj.id]);  // これでは消えてくれません
					// 1つのマーカーを消すのに、面倒でも以下の2行が必要
					var deleteList = markers.splice(i, 1);					
					leafletView.RemoveMarkers(deleteList);

					// 以下失敗例リスト
					//leafletView.RemoveMarkers(markers[i].hashCode);  //これはダメ
					//leafletView.RemoveMarkers(markers[obj.id],'item');
					//leafletView.ProcessView(); // 試しに入れてみる
					//leafletView.RemoveMarkers(markers[i-1]);
					//leafletView.RemoveMarkers(markers);					
					break;
				}
			}
			obj.lat = 91.0;
			obj.lng = 181.0;
			var json_obj = JSON.stringify(obj);
			ws.send(json_obj);				
		} else {
			// 位置情報更新
			console.log("else")
			for (let i = 0; i < markers.length; ++i) {
				if (obj.id == markers[i].hashCode){
					var ll = markers[i].position;
					ll.lat = obj.lat;
					ll.lng = obj.lng;
					break;
				}
			}
			var json_obj = JSON.stringify(obj);
			ws.send(json_obj);	
		}
	}

	// 位置情報の更新
    window.setInterval(function () {
        leafletView.ProcessView();  // 変更が行われたときに呼び出されれなければならない
	}, 1000);

	// サーバを止めると、ここに飛んでくる
	ws.onclose = function(event) {
		//print("CLOSE");
		ws = null;
	}


    map.addLayer(leafletView);
</script>



</body>
</html>
`))
// pm_proxy5_single_socket.go

/*
	このプログラムは、CSVファイルに記載された順番にエージェントの位置情報を読み取り、
	WebSocket を介してサーバーへ送信・更新する。

	サーバへ転送するプログラムは、server22-1.go

	- WebSocket 切断時に自動で再接続(最大5回リトライ)
	- エージェントが移動した場合のみサーバーへ通知
	- `sync.Mutex` を使用してスレッド安全な処理を実現
	- 位置情報が 999.0 以上の場合、エージェントを削除


	csv情報のサンプル (IDと緯度経度のみ使っている

	50044035447,139.629538,35.365357,00:00:00,WALK
	50044035447,139.629430,35.365151,00:00:30,WALK
	50044035447,139.629321,35.364945,00:01:00,WALK
	50044035447,139.629213,35.364738,00:01:30,WALK
	50044035447,139.629104,35.364532,00:02:00,WALK
	50044035447,139.628996,35.364325,00:02:30,WALK
	50044035447,139.628888,35.364119,00:03:00,WALK
	50044035447,139.628787,35.363937,00:03:30,WALK
	50044035447,139.628742,35.364159,00:04:00,WALK
	50044035447,139.628699,35.364375,00:04:30,WALK
	50044035447,139.628654,35.364592,00:05:00,WALK
	50044035447,139.628533,35.364724,00:05:30,WALK
	50044035447,139.628261,35.364691,00:06:00,WALK
	50044035447,139.627989,35.364658,00:06:30,WALK
	50044035447,139.627716,35.364625,00:07:00,WALK
	50044035447,139.627680,35.364620,00:07:30,WALK
	50044035447,999.9,999.9,00:08:00,WALK

	(50044035447, 999.9, 999.9, 00:08:00, WALK) でエージェントの移動が終了し、サーバからアイコンが消滅する

*/

package main

import (
	"encoding/csv"
	"flag"
	"fmt"
	"log"
	"math"
	"net/url"
	"os"
	"strconv"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	TopLeftLat     = 35.37574882601201
	TopLeftLon     = 139.61403393574466
	BottomRightLat = 35.36163108058289
	BottomRightLon = 139.6297897196359
)

type GetLoc struct {
	ID  int     `json:"id"`
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

type unmTbl struct {
	uniName string
	objType string
	simNum  int
	pmNum   int
	lon     float64
	lat     float64
}

var list = make([]unmTbl, 0)
var addr = flag.String("addr", "0.0.0.0:8080", "http service address")

var mutex sync.Mutex

func connectWebSocket() (*websocket.Conn, error) {
	u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo2"}

	for i := 0; i < 5; i++ {
		c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
		if err == nil {
			log.Println("Reconnected to WebSocket successfully.")
			return c, nil
		}
		log.Println("WebSocket reconnection attempt failed:", err)
		time.Sleep(2 * time.Second)
	}
	return nil, fmt.Errorf("failed to reconnect WebSocket after multiple attempts")
}

func main() {
	file, err := os.Open("agent_track_2018_101_end_and_order.csv")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	fmt.Println("Opened testtest.csv")
	reader := csv.NewReader(file)
	var line []string

	flag.Parse()
	log.SetFlags(0)

	c, err := connectWebSocket()
	if err != nil {
		log.Fatal("WebSocket connection failed:", err)
	}
	defer c.Close()

	for {
		time.Sleep(time.Millisecond * 10)

		line, err = reader.Read()
		if err != nil {
			break
		}

		uniName := line[0]
		objType := line[4]
		lon, _ := strconv.ParseFloat(line[1], 64)
		lat, _ := strconv.ParseFloat(line[2], 64)

		flag := 0

		for i := range list {
			if list[i].uniName == uniName {
				old_lat := list[i].lat
				old_lon := list[i].lon

				list[i].lon = lon
				list[i].lat = lat

				if math.Abs(list[i].lat-old_lat) > 0.000000001 || math.Abs(list[i].lon-old_lon) > 0.000000001 {
					gl := GetLoc{ID: list[i].pmNum, Lat: list[i].lat, Lng: list[i].lon}

					mutex.Lock()
					err = c.WriteJSON(gl)
					if err != nil {
						log.Println("write2 error:", err)
						mutex.Unlock() // **ロック解除**
						c.Close()
						c, err = connectWebSocket()
						if err != nil {
							log.Println("WebSocket reconnection failed:", err)
							return
						}
						continue
					}

					gl3 := new(GetLoc)
					err = c.ReadJSON(gl3)
					if err != nil {
						log.Println("read error:", err)
						mutex.Unlock() // **ロック解除**
						c.Close()
						c, err = connectWebSocket()
						if err != nil {
							log.Println("WebSocket reconnection failed:", err)
							return
						}
						continue
					}
					mutex.Unlock()

					println("move Object!")

					if list[i].lat > 999.0 || list[i].lon > 999.0 {
						list = append(list[:i], list[i+1:]...)
						println("-----------------------------------------------------------------> delete Object!")
					}
				}
				flag = 1
				break
			}
		}

		if flag == 0 {
			uniNum := len(list)
			ut := unmTbl{uniName: uniName, objType: objType, simNum: uniNum, lat: lat, lon: lon}

			gl := new(GetLoc)
			gl.ID = 0
			gl.Lat = ut.lat
			gl.Lng = ut.lon

			mutex.Lock()
			err := c.WriteJSON(gl)
			if err != nil {
				log.Println("write1 error:", err)
				mutex.Unlock() // **ロック解除**
				c.Close()
				c, err = connectWebSocket()
				if err != nil {
					log.Println("WebSocket reconnection failed:", err)
					return
				}
				continue
			}

			gl2 := new(GetLoc)
			err = c.ReadJSON(gl2)
			if err != nil {
				log.Println("read error:", err)
				mutex.Unlock() // **ロック解除**
				c.Close()
				c, err = connectWebSocket()
				if err != nil {
					log.Println("WebSocket reconnection failed:", err)
					return
				}
				continue
			}
			mutex.Unlock()

			ut.pmNum = gl2.ID
			list = append(list, ut)
			println("------------>generate Object!")
		}
	}
}

 

2025,江端さんの技術メモ

vscodeでデバッグをする時に、同じディレクトリにmain()関数を持つGo言語プログラムが複数あると、デバッグができません。ファイル名を指定してデバッグすることはできないでしょうか。ただし、launch.json にファイル名を書き込むと、実行ファイルが固定されてしまうので、それは避けたいのですが。

launch.json を使うと、特定のGoファイルが環境として固定されるため、別のファイルをデバッグしたいときに launch.json を都度編集する必要があり、やや不便です。

ですが、以下の方法を使えば 環境を固定せずに、デバッグするファイルを柔軟に切り替えられます。


1. launch.json${file} を使う

環境を固定せずに、現在開いているGoファイルをデバッグする方法です。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Current File",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${file}"
        }
    ]
}

  • ${file} は、現在アクティブな(開いている)ファイルをデバッグ対象にする特殊変数です。
  • この設定にすると、エディタで開いているGoファイルがどれでも、F5 でデバッグが開始できます。

開いているGoのプログラムでデバッガーが動いた。これから作業がラクになるかな。

2025,江端さんの忘備録

さすがに3つの案件の同時並列処理は無理 ―― と、泣き言を言いながら、今、本当に泣きそうです。

I'm whining that “it's impossible to handle three projects simultaneously,” and I'm about to cry.

経験的に、こういう時『もっと大変な人が、沢山いる』という考え方をすることは、悪手です。

Empirically, thinking that “there are many people in worse situations” is a bad idea in a situation like this.

他人なんぞ、どーでもいい。

I don't care about other people.

「私が辛い」という状態だけで、必要かつ十分です。

The fact that I am in pain is sufficient.

-----

そういえば、今年の年賀状には、長女の結婚式の写真の下に、自分のコメントとして

Speaking of which, on this year's New Year's card, under the photo of my eldest daughter's wedding, I remember I wrote the following  comment 

『昨年、"うつ"になりました』

'I became depressed last year.'

と書いたのを思い出しました。

『これでは、長女の結婚で"うつ"になったように読める』と気がついたのは、年賀状をポストに投函した後でした。

It was only after I had posted the New Year's card in the post that I realized that it could be read as if I had become depressed because of my eldest daughter's marriage.

しかし、私を"うつ"にするのは、いつだって、仕事です。

However, it is always work that makes me depressed.

その正体の多くが、動かないプログラムであり、停止するシステムです。

Many are programs that don't work and systems that become stop.

世の中には「頼めない仕事」というものが、あるのです。

There are some jobs that I can't ask someone to do.

つまりですね、技術というのは、属人性が高く、他人への譲渡が恐しく困難なのです

2025,江端さんの技術メモ

bytes-received send-rate-mbps receive-rate-mbps bandwidth-mbps rtt-ms cb
0 0 0 12 100 1706
0 1.128166 0 6.216 43.597 2000
0 3.958326 0 10.008 80.069 1831
0 4.864244 0 69.084 74.593 2000

この表にでてくる、send-rate-mbps receive-rate-mbps bandwidth-mbps の単位は何ですか

表にある send-rate-mbpsreceive-rate-mbpsbandwidth-mbps の単位は「Mbps(メガビット毎秒)」 です。

各項目の意味と単位

項目 意味 単位
send-rate-mbps 送信レート(今使っている帯域) Mbps(メガビット毎秒)
receive-rate-mbps 受信レート(今使っている帯域) Mbps(メガビット毎秒)
bandwidth-mbps 推定利用可能帯域(まだ使える帯域) Mbps(メガビット毎秒)

1. send-rate-mbps(送信レート)

  • 実際に送信しているデータ量の速度 を表す。
  • 今使っている帯域(現在の送信速度)。
  • 例えば send-rate-mbps = 4.864244 なら、約 4.86 Mbps でデータを送信している。

2. receive-rate-mbps(受信レート)

  • 実際に受信しているデータ量の速度 を表す。
  • 今使っている帯域(現在の受信速度)。
  • このデータでは receive-rate-mbps = 0 なので、受信データはない。

3. bandwidth-mbps(推定帯域幅)

  • SRTプロトコルが計算した「利用可能な最大帯域」
  • まだ使える帯域の推定値
  • 例えば bandwidth-mbps = 69.084 なら、SRTは「この回線では約 69.08 Mbps まで使える」と推定している。

単位換算

1 Mbps(メガビット毎秒)は:

  • 1,000 kbps(キロビット毎秒)
  • 1,000,000 bps(ビット毎秒)
  • 125 KB/s(キロバイト毎秒)
  • 0.125 MB/s(メガバイト毎秒)

例えば:

  • send-rate-mbps = 4.86 Mbps
    • = 4,860 kbps
    • = 約 607.5 KB/s
    • = 約 0.607 MB/s

結論

  • send-rate-mbpsreceive-rate-mbpsbandwidth-mbps の単位は「Mbps(メガビット毎秒)」
  • send-rate-mbps は今使っている送信帯域
  • receive-rate-mbps は今使っている受信帯域
  • bandwidth-mbps はSRTが推定した利用可能な最大帯域

このデータから、送信レートは 最大 4.86 Mbps であり、ネットワークが 最大 69 Mbps まで利用可能 と推定されていることが分かります。

2025,江端さんの技術メモ

GStreamerのC言語のライブラリの、res = gst_structure_get_double(stats, "bandwidth-mbps", &bandwidth_mbps); g_print("bandwidth-mbps: %d %f\n", res, bandwidth_mbps);の値が何の値を返しているのか教えて下さい

このプログラムにおける bandwidth_mbps の値は、SRT(Secure Reliable Transport)通信における推定帯域幅(Mbps) を示していると考えられます。


bandwidth_mbps の取得と用途

1. どこで取得しているか

bandwidth_mbps は、以下のコードで取得されています。

c
res = gst_structure_get_double(stats, "bandwidth-mbps", &bandwidth_mbps);
g_print("bandwidth-mbps: %d %f\n", res, bandwidth_mbps);

この部分では、GStreamer の srtserversink から取得できる SRTの統計情報 (stats) から "bandwidth-mbps" というキーの値を取得しています。

GStreamer の SRTプラグイン (srtserversink) は、SRTプロトコルのメトリクス情報を提供し、その中に "bandwidth-mbps" というパラメータが含まれています。

つまり、SRT通信時の「利用可能な推定帯域幅」をリアルタイムで取得している。

bandwidth_mbps の意味

  • SRTの推定帯域幅(Mbps)
  • リアルタイムでSRTの通信状況から取得される
  • 現在のネットワーク環境で送信可能な最大帯域を示す

bandwidth_mbps の用途

  • エンコードのビットレート調整に使用
  • ネットワークの状況に応じて、適切なビットレートを決定

「SRTの推定帯域幅(Mbps)」というのは、「今使っている帯域なのか」、「まだ使える帯域なのか」どちらでしょうか

bandwidth_mbps は、SRT (Secure Reliable Transport) における「推定帯域幅 (Estimated Bandwidth)」を示す値であり、その意味合いは 「まだ使える帯域」= 利用可能な推定帯域 に近いものです。


1. bandwidth_mbps は「今使っている帯域」ではない

理由 ①: "send-rate-mbps" や "receive-rate-mbps" という項目が別にある

プログラム内では、SRTの統計情報 (stats) から bandwidth-mbps の他に、send-rate-mbpsreceive-rate-mbps という値も取得できるようになっています。

c
res = gst_structure_get_double(stats, "send-rate-mbps", &send_rate_mbps);
g_print("send-rate-mbps: %d %f\n", res, send_rate_mbps);

res = gst_structure_get_double(stats, "receive-rate-mbps", &receive_rate_mbps);
g_print("receive-rate-mbps: %d %f\n", res, receive_rate_mbps);

  • send-rate-mbps → 現在の送信レート(今実際に使っている帯域)
  • receive-rate-mbps → 現在の受信レート(今実際に使っている帯域)
  • bandwidth-mbpsSRTプロトコルが推定した、利用可能な最大帯域

このことから、bandwidth-mbps は「現在使っている帯域」ではなく、「ネットワークが許容できる帯域の推定値」 であると考えられます。


2. bandwidth-mbps の定義(SRT公式の仕様より)

SRTの公式仕様では、"Estimated Bandwidth"(推定帯域)は次のように説明されています。

bandwidth-mbps: The estimated maximum available bandwidth of the connection, calculated by the SRT protocol based on observed network conditions such as packet loss and RTT.

要点

  • 「ネットワークの状況(パケットロスやRTTなど)を基にSRTが計算した、利用可能な最大帯域」 である。
  • 現在の送信レートとは異なる
  • "available bandwidth"(利用可能な帯域) という表現が使われている。

つまり、SRTが「この回線ではこのくらいの帯域が利用できるはずだ」と推定している値 であり、今実際に使用している帯域とは違う ということです。

4. まとめ

指標 意味 役割
send-rate-mbps 今使っている帯域(送信速度) 実際の送信速度をモニタリング
receive-rate-mbps 今使っている帯域(受信速度) 実際の受信速度をモニタリング
bandwidth-mbps まだ使える帯域(推定値) SRTが計算した利用可能な最大帯域

結論

bandwidth_mbps は「まだ使える帯域」= 利用可能な推定帯域
SRTがネットワーク状況を解析して算出した、回線が許容できる最大帯域を示している
「今使っている帯域(現在の送信速度)」ではなく、「どのくらいの帯域が利用可能か」という指標

この値を使って、適切なビットレートを動的に調整し、映像配信を安定させる仕組みになっています。

2025,江端さんの技術メモ

GStreamer (gst-launch-1.0) と tc (Traffic Control) の "bitrate" の意味が異なるため、混乱するのは自然です。以下に、それぞれの設定が どの部分を制御しているのか を整理して説明します。


1. GStreamer (bitrate=1700) の意味

x264enc bitrate=1700
  • GStreamer の bitrate=1700 は、エンコード時のビットレートを指定 しています。
  • 単位は kbps(キロビット毎秒) なので、この場合 1.7 Mbps になります。
  • エンコード後の映像ストリームのデータ量を決定 し、これが ネットワークに流れるデータ量の目安 になります。
  • つまり、GStreamer の設定は「この映像ストリームのビットレートを 1.7 Mbps にしなさい」という指示 です。

換算

  • 1700 kbps = 1.7 Mbps
  • 1.7 Mbps = 約 0.212 MB/s(メガバイト毎秒)
    • 計算式: 1.7 Mbps ÷ 8 = 0.212 MB/s

2. tc (rate 5mbit) の意味

tc qdisc add dev eth0 root tbf rate 5mbit burst 10kb latency 50ms
  • tcrate 5mbit は、ネットワークインターフェース (eth0) の最大帯域を制限する設定 です。
  • 単位は Mbps(メガビット毎秒) なので、この場合 5 Mbps になります。
  • これは 「eth0 から送信できる最大速度を 5 Mbps に制限しなさい」という指示 です。
  • つまり、GStreamer が 1.7 Mbps の映像を送信しようとしても、5 Mbps の帯域制限があるため、この範囲内なら問題なく送信できる

換算

  • 5 Mbps = 約 0.625 MB/s(メガバイト毎秒)
    • 計算式: 5 Mbps ÷ 8 = 0.625 MB/s

3. GStreamer bitrate=1700tc rate=5mbit の関係

(1) GStreamer のビットレートが tc の制限内に収まる場合

  • GStreamer (bitrate=1700 kbps = 1.7 Mbps)
  • tc の制限 (rate=5mbit = 5 Mbps)
  • 1.7 Mbps < 5 Mbps → 問題なく通信できる
  • 映像ストリームのデータ量は 1.7 Mbps なので、帯域 5 Mbps では十分

(2) GStreamer のビットレートが tc の制限を超える場合

例えば、GStreamer の bitrate=6000(6 Mbps)と設定した場合:

  • 6 Mbps > tcrate=5mbit
  • 制限(5 Mbps)を超えているため、パケットロスが発生する可能性がある
  • 映像が乱れたり、ストリームが停止する可能性がある

このように、GStreamer の bitrate は映像のエンコード後のデータ量、tcrate はネットワークの最大許容量を決める ため、両者の関係を考える必要があります。


4. まとめ

設定 役割 値の意味 設定値(例)
GStreamer bitrate 映像エンコードのビットレート 送信するストリームのデータ量 bitrate=1700 → 1.7 Mbps
tc rate ネットワークの最大帯域制限 インターフェースの最大通信速度 rate=5mbit → 5 Mbps

結論

  • GStreamer の bitrate は「どれだけのデータを送るか」
  • tcrate は「送れるデータ量の上限」
  • GStreamer のビットレートが tc の制限内なら問題なし(1.7 Mbps < 5 Mbps)
  • GStreamer のビットレートが tc の制限を超えるとパケットロスの可能性あり(例: 6 Mbps > 5 Mbps)

つまり、現在の設定(bitrate=1700rate=5mbit)では 1.7 Mbps の映像を 5 Mbps の帯域内で余裕をもって送信できる ということになります。

よし、これは分かった。

2025,江端さんの技術メモ

添付ファイルのように、解像度やフレームレートが決まっているので、ビットレートは自動的に決まるような気がします。「1クライアントあたりのビットレート」とは何でしょうか?

「1クライアントあたりのビットレート」とは、ストリーミング配信時に1つのクライアント(視聴者)に対して割り当てられるビットレートを指します。

考えられる動作

  • 固定ビットレート(CBR: Constant Bitrate)の場合

    • 設定したビットレート(例:3072kbps)が、そのまま1クライアントあたりの配信ビットレートとして適用されます。
    • クライアントが増えても、各クライアントに同じビットレートで送信されるため、サーバーの総帯域は**「3072kbps × 接続クライアント数」**となる。
  • 可変ビットレート(VBR: Variable Bitrate)の場合

    • ネットワークの状態や映像の動きによってビットレートが変動する可能性があります。
    • クライアントごとに適応的に異なるビットレートが設定されることがある。

サーバー側の負荷との関係

  • クライアント数が増えると、総帯域も増加
    例えば、10人のクライアントが接続すると、合計ビットレートは 3072kbps × 10 = 30.72Mbps になります。
    サーバーの帯域幅が限られている場合、クライアント数が増えるとストリーミングの品質が低下する可能性があります。

  • マルチキャスト配信(UDP)であれば帯域が一定
    画像の設定では「インターネットモード:Off(UDP)」になっています。
    UDPマルチキャストを使う場合、同じストリームを複数のクライアントに送信できるため、クライアント数が増えてもサーバーの総帯域には影響しにくいです。

設定の意味

この設定が「1クライアントごとのビットレート」になっているため、

  • クライアントごとに3072kbpsで配信される
  • クライアント数が増えると、サーバーの帯域も比例して増加
  • UDP(マルチキャスト)であれば、帯域が固定されて効率的に配信可能

という仕組みになっていると考えられます。

つまり「1クライアントあたりのビットレート」というのは「1クライアントあたりのビットレートの上限値」という解釈で良いですか。つまり、それ以上のビットレートは出せない、という意味である、ということで良いですか?

「1クライアントあたりのビットレート」=「1クライアントあたりのビットレートの上限値」 という解釈でほぼ正しいです。
つまり、設定したビットレートを超えるデータ量は送信されない という意味になります。

ただし、可変ビットレート(VBR) の場合は少し異なり、最大値としての上限 になりますが、実際のビットレートは映像の内容に応じて変動 します。
逆に、固定ビットレート(CBR) では 常に設定値で送信 されます。

具体的な挙動

配信モード 「1クライアントあたりのビットレート」の意味
固定ビットレート(CBR) 常に設定値のビットレートで送信(超えないし、下回らない)
可変ビットレート(VBR) 最大値としての上限(超えないが、低くなることはある)
フレームレート指定 フレームレートを固定しつつ、CBRまたはVBRを適用
ベストエフォート指定 ネットワーク状況に応じて変動(上限なしの場合も)

結論

「1クライアントあたりのビットレート」は、基本的には「上限値」として機能し、設定値を超えることはない
ただし、ビットレートの挙動は配信モードに依存する ため、VBRでは上限値未満で可変、CBRでは常に設定値 という違いがある。

今回の場合、ユーザは数人程度しかいません。
配信モードは「固定ビットレート」「可変ビットレート」「フレームレート指定」「ベストエフォート指定」のメニューがあります。
解像度は640x360で、5fps,10fps,20fpsの3種類の転送実験を行いたいと思っています。

選択すべき配信モード、解像度とfpsに対する、1クライアントあたりのビットレートを表にして示して下さい

推奨設定とビットレート表

1. 配信モードの選択

  • 「フレームレート指定」 が適切
    • 実験の目的が「解像度 640×360」で 5fps, 10fps, 20fps の転送実験 を行うことなので、フレームレートを固定できるモードを選択するのが適切。
    • 「固定ビットレート」では画質が保証されず、特に低fps時に無駄なビットレート消費が発生する可能性がある。
    • 「ベストエフォート指定」ではネットワーク状況に応じて変動するため、実験の統一性が損なわれる可能性がある。
    • 「可変ビットレート(VBR)」も画質を維持しやすいが、フレームレートとの相性が不明なため、今回の実験には適さない可能性がある。

2. 各設定における推奨ビットレート

解像度 フレームレート 推奨ビットレート(H.264, 1クライアントあたり)
640×360 5fps 93kbps(最低100~150kbps推奨)
640×360 10fps 185kbps(最低200~300kbps推奨)
640×360 20fps 370kbps(最低400~600kbps推奨)

3. 補足

  • ビットレートの決定方法
    • 参考値として フルHD(1920×1080, 30fps)の5000kbps を基準に、解像度とfpsの比率を使ってスケーリングして計算。
    • 実際の設定では、若干高めのビットレート(表の推奨範囲)を指定した方が安定する。
  • 配信モードの調整
    • 「フレームレート指定」+「固定ビットレート」(上記の推奨値)で安定した画質を維持しやすい。
    • ネットワーク帯域に余裕があれば 「可変ビットレート(VBR)」を試して、最低ビットレートを表の値以上に設定する ことで、より滑らかな映像が得られる可能性もある。

4. 最適な設定の結論

設定項目 推奨値
配信モード フレームレート指定
解像度 640×360
フレームレート 5fps, 10fps, 20fps(それぞれ実験)
1クライアントあたりのビットレート 最低値を超えるよう設定(推奨範囲:100~600kbps)

結論:

  • 実験では「フレームレート指定」モードを選択
  • 各fpsに対応する最低ビットレートを超えるように設定
  • ネットワークに余裕があれば、少し高めのビットレート(推奨範囲内)を選択
  • 可変ビットレートも試してみる価値あり(最低値を超える設定で)

2025,江端さんの技術メモ

DockerのPostgreSQLデータベースをWindows→WSLのDockerへ移行する方法

Windows上のDockerで動作するPostgreSQLデータベースを、WSL(Windows Subsystem for Linux)のDockerに移行する 手順を説明します。


🔷 方法 1: Dockerボリュームごと移行する(推奨)

Windowsでは grepbusybox は不要 で、標準的な PowerShellコマンド を使用して移行できます。

📌 ① ボリューム名を確認

まず、PostgreSQLのデータが保存されているボリューム名を確認します。

powershell
docker volume ls

出力例:

DRIVER VOLUME NAME
local hakata_db-data

この hakata_db-data がPostgreSQLのデータが格納されているボリュームです。


📌 ② ボリュームをtarファイルにバックアップ

PowerShellを管理者権限で開き、以下のコマンドを実行します。

docker run --rm -v hakata_db-data:/data -v C:\Users\tomoi\backup:/backup ubuntu tar czf /backup/hakata_db_backup.tar.gz -C /data .

重要なポイント

  • C:\Users\tomoi\backup はWindows側のバックアップ保存先(任意のフォルダを指定)
  • ubuntu イメージを使用(busybox は不要)
  • hakata_db_backup.tar.gz という圧縮ファイルが作成される

この hakata_db_backup.tar.gz をUSBやネットワーク経由で 移行先のマシンへコピー します。


📌 ③ WSLのDocker環境にファイルをコピー

WSLのファイルシステムにバックアップを移動します。

powershell
wsl

WSL内で以下のコマンドを実行して、WindowsのバックアップファイルをWSLの /home/ ディレクトリにコピー。

cp /mnt/c/Users/tomoi/backup/hakata_db_backup.tar.gz /home/

📌 ④ WSLのDockerでボリュームを作成

WSL内で以下を実行し、新しいボリュームを作成します。

docker volume create hakata_db-data

作成したボリュームにデータを展開します。

docker run --rm -v hakata_db-data:/data -v /home:/backup ubuntu tar xzf /backup/hakata_db_backup.tar.gz -C /data

📌 ⑤ WSLのDockerでPostgreSQLコンテナを起動

WSL内で以下を実行し、データをマウントしたPostgreSQLコンテナを起動。

docker run -d --name postgres_hakata -p 15432:5432 -v hakata_db-data:/var/lib/postgresql/data postgres:16

📌 ⑥ データが正しく復元されたか確認

PostgreSQLコンテナに接続してデータを確認。

docker exec -it postgres_hakata psql -U postgres

データベース一覧を表示:

\l

2025,江端さんの技術メモ

音声を無視して、最も一般的な webm → mp4 変換コマンドは以下のようになります。

ffmpeg -i input.webm -vf "scale=640:360" -r 5 -c:v libx264 -preset slow -crf 23 -an output.mp4

説明:

  • -i input.webm:入力ファイルの指定
  • -vf "scale=640:360":解像度を640x360に変更
  • -r 5:フレームレートを5fpsに変更
  • -c:v libx264:H.264コーデックでエンコード
  • -preset slow:エンコード速度と圧縮率のバランスをとる(品質優先)
  • -crf 23:品質を指定(数値が低いほど高品質)
  • -an:音声を削除(無音にする)
  • output.mp4:出力ファイルの指定

このコマンドで、音声なしの mp4 に変換できます。