今回は、「整数型で高方程式を解く=分散並列処理=」というタイトルの投稿を予定していました。しかし投稿用のsocket通信のプログラムを作成しているとき「予期せぬ出来事」があり、先に報告することにしました。
この「予期せぬ出来事」は、今回のタイトルにある「TCP関連のkernelパラメータ」に起因するものでした。最初は整数型の長大な桁数のdataを送受信できるプログラムということで、送信する数字のリストをどんどん大きくしていきました。初めはclient側送信量とserver側受信量はピッタリ一致していました。しかしある時点から、client側からの送信量に比べて、server側での受信量が少なくなる現象が出始めました。以下に経過を示します。Raspberry Pi 4Bのメモリー8GBの機種で、Raspberry Pi OS(2020-AUG-23, 64bitベータ版)に同梱のThonnyを使っています。
#予期せぬ出来事
tcpのsocket通信をするための、serverプログラムとclientプログラムを、以下に示しています。
import socket
import time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('192.168.2.20', 60020))
s.listen(5)
clientsocket, address = s.accept()
print(f"Connection from {address}")
list0en = b''
list0en = clientsocket.recv(1016)
list0str = list0en.decode("utf-8")
len_list0str = int(list0str)
print(len_list0str)
initial_time=time.time()
q, mod = divmod(len_list0str, 1016)
list1stren = b''
if mod > 0:
q += 1
for i in range(q):
list1stren += clientsocket.recv(1016)
print('recv finished')
list1str = list1stren.decode("utf-8")
print(str(time.time() - initial_time))
if len_list0str==len(list1str):
print('length OK')
clientsocket.close()
s.close()
print(list1str)
time.sleep(10)
import socket
import time
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.connect(('192.168.2.20', 60020))
tmp_0=1234567890123456
list0 = []
Su16b=800#14,400byte OK
#Su16b=1000#18,000byte loss
#Su16b=10000#180,000byte loss
#Su16b=100000#1.75MB loss
#Su16b=600000#10MB loss
for i in range(Su16b):
list0.append(tmp_0)
list0str = str(list0)
len_list0str = len(list0str)
len_list0str_str = str(len_list0str)
s1.send(bytes(len_list0str_str, 'utf-8'))
q, mod = divmod(len_list0str, 1016)
for i in range(q):
temp_21str = list0str[1016*i:1016*(i+1)]
s1.send(bytes(temp_21str, 'utf-8'))
if mod > 0:
temp_21str = list0str[1016*q:]
s1.send(bytes(temp_21str, 'utf-8'))
s1.close()
time.sleep(10)
clientプログラム中にSu16b=800#14,400byteがありますが、ここまでは問題なく送受信できていました。次にSu16b=1000#18,000byteと送信量を増やした時にserver側で受信するbyte数が少ない現象が出ました。Su16b=10000と更に増やしてもserver側で受信するbyte数が足りません。送信量が14,400byteより少し増えると受信量が合わなくなります。更にserverプログラムはsocketを閉じたはずですが、60020のportが使用中のままです。RaspberryPi本体を再起動して回復しました。再びserverプログラムを動かし受信すると、再び受信量が合わない、そして再び60020のportも使用中のままの繰り返しです。
ここで問題が2点生じています。1つはserver側の60020のportが使用中のままであること。もう1つは送信量より受信量が少ないことです。以下で別々に解決していきます。
#使用中のportを開放
今回はRaspberryPi本体を再起動することで解決しました。
#少ない受信を解決
初めに思いついたのがよく見かけるバッファーもれです。さらに「送信量が14,400byteより少し増えると受信量が合わなくなる。」ことに着目しました。受信バッファーが足りないと予想しました。そこでkernelパラメータが見れるsysctlコマンドで確認しました。kernelパラメータはふれたこともありませんから少し気が重かったのですが。
net.core.rmem_default=212992
net.core.wmem_default=212992
net.ipv4.tcp_rmem=4096 131072 6291456
net.ipv4.tcp_wmem=4096 16384 4194304
受信バッファーrmemはdefaultが131,072byteもありますから14,400byte付近からのバッファーもれとは関係なさそうです。一方、送信バッファーwmemのdefaultが16,384byteです。14,400byte付近からのバッファーもれに近いです。受信バッファーもれではなく、送信バッファーもれだと仮定しました。もともと受信バッファーは自動的な拡張がデフォルトでONになっているので受信バッファーもれは生じないようです。送信バッファーもれだと仮定すると、送信バッファーを増やせばいいことです。
まずpython.orgのソケットプログラミングの以下のページはあらかじめ読んでいました。「socketを確実にcloseする」ように書かれていました。さらに「(信じられないかもしれないが) 一回の recv で 5 文字を全部受け取ることができるとは限らないからだ。・・・高負荷ネットワークのもとでは、recv ループをふたつ使わないコードは、あっと言う間にダメになってしまう」の部分は、高負荷になるとあるのかもしれないと参考にしてプログラムを書きました。
ソケットプログラミング HOWTO 著者:Gordon McMillan
そして今回のバッファーもれ現象です。最初にkernelパラメータを変えていいのだろうかと色々調べました。よく見かけるのがWebサーバーのチューニングです。kernelパラメータをどんどん変えています。以下のページも参考にしました。
Linuxカーネルのチューニング .github
synアタックプロテクトなど公開Webサーバー向けのチューニングなども含まれていました。しかし今回は奥の奥にある閉鎖的なLANで、さらに送受信方法も決めたTCP通信です。公開サーバー向けのsynアタックなどは関係ありません。今回は送信量増大に伴う、送受信バッファー関連だけ変更しました。
mousepadでetcフォルダーのsysctl.confファイルを開き、以下の6行を追加しました。
net.core.wmem_default=4194304
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 4194304 16777216
net.ipv4.tcp_wmem=4096 4194304 16777216
RaspberryPi本体を再起動後、前述のserverプログラムとclientプログラムを実行したところ、10MBまで正確に送受信できるようになりました。ただバッファーMAXが16MB程ですから、送信量が10MBを超えた場合は、分割して送信する必要があることもわかりました。
今回のkernelパラメータ変更の問題点: Raspberry Piでインターネットを見ることは無いです。しかし仮に、この設定でインターネットを見るとします。例えば10kbyteの小さな画像アイコンが100種類配置されたWebページを閲覧したとします。Webページに配置された100個の画像のリクエストのため、100x送信バッファー4,194,304byte、100x受信バッファー4,194,304byte、合計で約840MBのメモリーを瞬間的に使うことになります。但しメモリーを使い過ぎるとsocket毎がminの4,096byteに自動的に縮小されるようなのでフリーズは回避できると思います。しかし受信バッファー4,096byteで10kbyteの画像ファイルを受信しなければならなくなります。多分受信もれが生じます。何かデコボコのチューニングです。これから考えると、今回のdefaultの4194304byteは、通常は大きくてもこの1/40程度、131072byteほどが推奨されているようです。またRaspberryPiでもメモリー1GBおよび500MBの機種では、少ないメモリーのためにチューニングがより繊細になりますから、チューニングの難易度がさらに上がりそうです。
今回は、ふれたこともないkernelパラメーターのTCP関連の設定変更になりました。整数型の長大な桁数のdataを送受信する目的で、支障が出ない最小範囲だけ変更したつもりですが、わかりません。投稿するのも少し気が重かったのですが、プログラム作成中に生じた1つの現象と試行した1つの解決策として今回報告しました。
肥大化した送受信量のプログラム中、バッファーもれにより離反化していく現象が生じ始め、順調に進んでいるように見えたプログラムに落日の予兆が、ぽつぽつと垣間見え始めていることがわかりました。
最後までご覧いただいて、ありがとうございました。
今回はQiitaの主旨に同意して投稿していますので、公開したプログラムはコピー、改変などでのご使用は自由です。著作権に関する問題も発生しません。
ただし、Raspberry Pi 4Bを使う場合にはCPUに特に大きめのヒートシンクが必須です。LAN通信の頻度の少ない今回のプログラムのような場合、LANチップは熱くなりません。しかし計算時間が継続するとCPUの激しい発熱で分かるように相当電力を使っています。電源UCB Cタイプの後ろにある黒い小さなチップも熱くなりますので、ファンでの風流も必要です。
#追加変更プログラム
後日、追加したプログラムです。tcp-while-server.pyとtcp-while-client.pyです。送信のclient側で送信タイミングを考慮し、受信のserver側は1coreフル稼働で受信しています。混雑していない閉鎖LANではうまくいきます。Raspberry Pi同士で9MB/sの転送速度です。ただ大容量2MB以上のファイルになると受信速度が落ちるので受信ファイルを分割しました。10MBファイルの転送速度は14MB/sに上がりました。さらに50MB、100MBと転送ファイルを大きくしたシーケンシャルモードの試行(list1stren...という変数を40個並べるだけなのでプログラム省略)では、18MB/s前後まで転送速度が上がりました。全て「length OK」となります。また変更していたkernelパラメーターはOSデフォルトに戻しました。
import socket
import time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('192.168.2.24', 60024))
s.listen(5)
clientsocket, address = s.accept()
print(f"Connection from {address}")
list0en = b''
list0en = clientsocket.recv(1046)
list0str = list0en.decode("utf-8")
len_list0str = int(list0str)
print(len_list0str)
initial_time=time.time()
q, mod = divmod(len_list0str, 14400)
list1stren1 = b''
list1stren2 = b''
list1stren3 = b''
list1stren4 = b''
list1stren5 = b''
if mod > 0:
q += 1
while True:
if len(list1stren1)+len(list1stren2)+len(list1stren3)+len(list1stren4)+len(list1stren5) >= len_list0str:
break
if len(list1stren1) < 2500000:
list1stren1 += clientsocket.recv(14400)
elif len(list1stren2) < 2500000:
list1stren2 += clientsocket.recv(14400)
elif len(list1stren3) < 2500000:
list1stren3 += clientsocket.recv(14400)
elif len(list1stren4) < 2500000:
list1stren4 += clientsocket.recv(14400)
else :
list1stren5 += clientsocket.recv(14400)
print(str(time.time() - initial_time))
print('recv finished')
list1str1 = list1stren1.decode("utf-8")
list1str2 = list1stren2.decode("utf-8")
list1str3 = list1stren3.decode("utf-8")
list1str4 = list1stren4.decode("utf-8")
list1str5 = list1stren5.decode("utf-8")
list1str = list1str1
list1str += list1str2
list1str += list1str3
list1str += list1str4
list1str += list1str5
if len_list0str==len(list1str):
print('length OK')
clientsocket.close()
s.close()
print(list1str)
print('Server closed')
time.sleep(10)
import socket
import time
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.connect(('192.168.2.24', 60024))
tmp_0=1234567890123456
list0 = []
#r_su=800#14,400byte
#r_su=1000#18,000byte
#r_su=10000#180,000byte
#r_su=100000#1,800,000byte
r_su=600000#10,800,000byte
for i in range(r_su):
list0.append(tmp_0)
list0str = str(list0)
len_list0str = len(list0str)
len_list0str_str = str(len_list0str)
s1.send(bytes(len_list0str_str, 'utf-8'))
time.sleep(0.002)#recv
q, mod = divmod(len_list0str, 14400)
for i in range(q):
temp_21str = list0str[14400*i:14400*(i+1)]
s1.send(bytes(temp_21str, 'utf-8'))
time.sleep(0.0001)
if mod > 0:
temp_21str = list0str[14400*q:]
s1.send(bytes(temp_21str, 'utf-8'))
s1.close()
time.sleep(10)
変更していたkernelパラメーターをOSデフォルトに戻します。追加していた6行をすべてコメントアウトします。
sudo mousepad /etc/sysctl.conf
mousepadでetcフォルダーのsysctl.confファイルを開き、以下の6行をコメントアウトしました。
#net.core.rmem_default=4194304
#net.core.wmem_default=4194304
#net.core.rmem_max=16777216
#net.core.wmem_max=16777216
#net.ipv4.tcp_rmem=4096 4194304 16777216
#net.ipv4.tcp_wmem=4096 4194304 16777216
RaspberryPi本体を再起動します。OSデフォルトに戻ります。
次回、「整数型で高方程式を解く=分散並列処理=」では、Raspberry Pi 4台のsocket通信を使った分散処理により、計算時間を短縮する試行を予定しています。