"も"日記

思考整理のためにいろいろ書き殴っていきます

【python】multiprocessとthreadを勉強したかった

業務で作成している生産管理ウェブシステムで、
入力された値を使って処理を実行するが、処理時間が長くなるので、
サーバー側での処理終了を待たずにユーザーにレスポンスを返す仕組みを実装したかった。

マルチプロセスやスレッドを使うべきなのはなんとなく分かるのだが、
これまで勉強してこなかったので、この際勉強して使えるようになろうと。

マルチスレッド(並行処理)とマルチプロセス(並列処理)の整理から

マルチプロセス

・実行中のプログラムのこと
・プロセス間でメモリの共有はない(プロセスごとに領域を確保)

メリット

・一つのプロセスでエラーが発生しても、ほかのプロセスには影響を及ぼさない。
・他のプロセスのことを考えなくてよいので、プログラミングミスが起きにくい

デメリット

・プロセス間でのデータ共有が困難
・生成に多くのオーバーヘッドを強いる

使いどころ

・処理を行う領域自体を分けて高速化したい

スレッド

・一つのプロセスの中に複数作成が出来る
・一つのプロセス内のスレッドはメモリ領域を共有する

メリット

・メモリの使用率が低い
・プロセスよりも少ない時間で生成できる

デメリット

・同時実行処理でデータ変更を行う場合には、排他ロック(GILなど)が必要
・一つのスレッドのクラッシュが全体に影響を及ぼす
・プログラミングミスが起きやすく、コーディングの難度が高い。

使いどころ

・レスポンスタイムを速くしたい
スループットを向上させたい

結局のところ、同一プロセス内で分岐させたいか、
そうでないかが争点になるのだろうか…

今回実装したい処理は、メイン関数の中でレスポンスと
サーバーでの内部処理を分岐させたいだけで、
プロセスを分ける必要はないので、スレッドを使って実装すればよい。

というわけでコードはこんな感じになった。

import threading

# メイン処理
def main(num):
    print('開始')
    # 新規スレッド生成
    p=threading.Thread(target=sub,args=(num,))
    # スレッド開始
    p.start()
    print('レスポンス')
    
# サブ処理
def sub(num):
    print('内部処理')
    for i in range(0,num):
        print(i)
    print('内部処理終了')
    
# メイン処理呼び出し
main(100)

実行結果

開始
内部処理
0
1
2
レスポンス
3
…
98
99
内部処理終了

因みにマルチプロセスだと以下になる。

# メイン
def main2(num):
    print('開始')
    # mainを明示(書かないとエラーになる)
    if __name__ == "__main__":
        # プロセス生成
        p=multiprocessing.Process(target=sub,args=(num,))
        # プロセス開始
        p.start()
    print('レスポンス')
    
# サブプロセス
def sub2(num):
    print('内部処理')
    for i in range(0,num):
        print(i)
    print('内部処理終了')
    
# メインプロセス呼び出し
main2(100)

【雑記】生産計画の変更は実績に応じて常に行うべきなのか

ここでいう生産計画は週間とか、一か月とかの中長期の的なものではなく、
ほとんど実行が確実な当日の生産スケジュールのお話。

仕事で作成中の生産管理システムがあるのだが、
その中には当然一日の生産スケジューリングの機能もあるわけで。
積み上げ式ではなく、完成工程から遡って計画時間を設定する、
そんなタイプのスケジューラーだ。

当初の設計では、計画の修正は行う予定はなかったのだが、
現場からは「進度に応じて計画時間を変更してほしい」なんて要望があった。

確かに、生産指示のためのスケジューラーだとするなら当然、常に実績と計画を照合し、
進んでいればスケジュールを前倒し、遅れれば後倒すのが作業者にとっては都合が良いのだろう。

しかし、それでは計画に対する実績の評価を行うことは困難になるし、
そもそも積み上げ式の生産管理をやめてダイアグラム的な生産スケジューラーを
導入しようとしているのに本末転倒なのではないか?

この計画を実績に合わせに行くというスタンスはどうも違和感を感じる。

「計画を達成するために今あるリソースを使って頑張る」が正しい形であると思うし、
計画を実績に合わせに行くようでは、計画の数字が正しいのか、基準工数は正しいのか、 ボトルネックはどこなのか分析のしようがないではないか。

もちろん、実績自体もスケジュールを構築するための一要素ではあるし、
一日の終わりとか半日とか単位を決めて清算をしていくことは不可欠だとは思うのだが…。

まあこの辺はどうしても現場担当者と業務設計者では目線が違う(悪い意味ではない)ので、
しょうがないことなのだけれども。

現実的な話をするのであれば、折り合いをつけられるように折衷案で行くしかないよねとなるのだが、
こっそり自分の思想は反映してやろうと思う今日この頃。

自分の考え方が正しいかどうかも知りたいしね(笑)。

データベースの設計とか

Excelで管理されているマスタをDBでの管理に移行するため、
DB構造を考える必要があったので考えたことを整理。

現状ではexcel管理の情報で、一つのセルに複数の項目が記載されていたり、
同じ項目なのに標記揺れがあったりとめちゃくちゃ…。
<現状のテーブル>

製品名 仕様
製品A AAA,BBB,CCC
製品B BBB,CCC,DDD
製品C AAA’,BBB,DDD,EEE

今は表示だけにしか使用されていないのでそれでも何とかなっているのだが、
項目の有無によっては製造工程が変化したり、製造順に関わる項目もあるので、
生産計画の立案にも使用していきたい。

<要件>
① 1製品ごとの仕様を記録
② 製品によって仕様項目・項目数が異なる
③ 仕様項目は増減が予想されている
④ 項目/内容の両方で検索できる

案①

全て一つのテーブルでまとめる。

イメージ

製品名 仕様① 仕様② 仕様③ 仕様④ 仕様⑤
製品A AAA BBB CCC 予備 予備
製品B BBB CCC DDD 予備 予備
製品C AAA’ BBB DDD EEE 予備

製品をキーにあらかじめ多めに仕様入力用のカラムを設定しておくタイプ。
これはまあ素人目に見てもNGとわかる設計。
列ごとで項目がそろっていないため、検索もできず、また標記揺れも防ぐことが
出来ないので全く用途に合っていない。
そもそもこれならExcelでいいよねって話になる。

案②

製品マスタ、項目マスタを作成し、中間テーブルで紐づける。

イメージ
<製品マスタ>

ID 製品名
1 製品A
2 製品B
3 製品C

<仕様項目マスタ>

ID 仕様項目
1 仕様項目①
2 仕様項目②
3 仕様項目③

<中間テーブル>

製品ID 仕様項目ID 仕様内容
1 1 AAA
1 2 BBB
1 3 CCC

このパターンでは、中間テーブルを用いることで、仕様項目での検索が
実現できており、一見要件を満たしているように見える…のだが、
choiceなどで仕様内容を縛れないので標記揺れが生じてしまう。
めちゃくちゃコードを工夫すれば、セレクトを生成してどうにか…みたいなことも
出来るのかもしれないが、メンテの少ないコードを書く自信が起きないので没。

案③

製品マスタ、仕様項目マスタに加えて、仕様内容マスタを作成し、仕様内容IDと製品ID中間テーブルで紐づける。 イメージ
<製品マスタ>

ID 製品名
1 製品A
2 製品B
3 製品C

<仕様項目マスタ>

ID 仕様項目
1 仕様項目①
2 仕様項目②
3 仕様項目③

<仕様内容マスタ>

ID 仕様項目ID 仕様内容
1 1 AAA
2 2 BBB
3 3 CCC

<中間テーブル>

製品ID 仕様内容ID
1 1
1 2
1 3

案②で解決できなかった標記揺れを解決できるように工夫した形。
項目を管理するテーブルと項目に関する内容を管理するテーブルを作成することで、
検索性を保ちながら標記揺れを防ぐことができるし、
項目IDでレコードを引っ張ってきてやるだけで、セレクトボックスを生成してやることもできる。
もちろん、マスタの登録者が先に内容テーブルを作成しなければいけないという手間は増えるが、
大量に入力されたデータを後からメンテしたり、コードのメンテしたりということを考えるとこれがベストなのかなと。

まあ、素人なりの最善ということ多少ガバでも許して…。

【gunicorn】logの設定について

gunicornのlog設定をいじったので忘備録。

1. logの基本設定

gunicornはデフォルトの状態ではlogを吐き出してくれない 。
なので、設定ファイルを作成して出力をonにしてあげなければいけない。

gunicorn_setting.py

# access log
accesslog = 'logを出力したいディレクトリ'
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

# gunicorn log
errorlog = 'logを出力したいディレクトリ'
loglevel = 'debug'

※loglevelは debuginfowarningerrorcritical から選択可能

2.logの抑制

下記のコマンドでgunicornを起動する。

$ gunicorn --config /path/path/gunicorn_settings.py

これでlogは出力できるようになったが、AWS上で開発環境を構築しているため、
ELBヘルスチェッカーのlogがaccesslogに常に出力され続けてうっとおしい…。

logの細かい制御は、gunicornがデフォルト持っているgloggingを継承してclassを作成し、
logger_classに渡してやればカスタムができるらしいので、設定ファイルに以下を追記。

gunicorn_setting.py

# logのフィルタリング設定
import logging
from gunicorn import glogging
class CustomGunicornLogger(glogging.Logger):
    def access(self, resp, req, environ, request_time):
        # ELB-HelthCheckerのログを除外
        if "ELB-HealthChecker" not in environ['HTTP_USER_AGENT']:
            super().access(resp, req, environ, request_time)
 
logger_class = 'config.gunicorn_setting.CustomGunicornLogger'

これでELBヘルスチェッカーのログは出力されなくなった。

3.logのローテーション

gunicornにはログローテーションの設定はデフォルトで存在しないので、
pythonのloggingを使って制御するか、osのlogrotatedを使うらしい。
調べたところ、logrotatedを使うほうが圧倒的に設定が楽なので、
/etc/logrotated/conf/に設定ファイルを作成。

/logファイルの存在するpath/ {
    daily
    rotate 3
    copytruncate
    compress
    missingok
    su root root 
}

はじめはcreateでオプションを設定していたが、うまくローテーションが実行されなかった。
createで新規ファイルを作成すると、作成されたファイルではなくローテーション先の
ファイルにlogを追記するという挙動が起きてしまった。
詳しいことは分からないが、gunicornは新規生成したファイルには同名であっても追記してくれないようだ。

su root rootは権限設定によっては、必要となるオプション。

$ sudo /usr/sbin/ logrotate -d /etc/logrotate.d/作成したlogrorotateの設定ファイル

を実行してみて、permissions errorが発生したらユーザーとグループを指定してあげるか、 ディレクトリそのものの権限設定を変更する必要があるらしい。