私はソフトウェアエンジニアとして約 15 年間働いています。私はキャリアを通じて多くのことを学び、その学んだことを多くの分散システムの設計と実装 (時には段階的に廃止したり、そのまま残したり) に応用しました。その過程で私は数多くの間違いを犯し、今でも間違いを犯し続けています。しかし、私の主な焦点は信頼性だったので、エラーの頻度を最小限に抑える方法を見つけるために自分の経験とコミュニティを振り返ってきました。私のモットーは、次のとおりです。新しい間違い (それほど明白ではありませんが、より高度な間違い) を絶対に犯してみなければなりません。間違いを犯すのは問題ありません。それを繰り返して学習するのです。それは悲しく、落胆させます。
おそらくそれが、私が常に数学に魅了されてきた理由です。それはエレガントで簡潔であるだけでなく、その論理的厳密さが間違いを防ぐためです。現在の状況、どのような仮説や定理を信頼できるかについて考える必要があります。これらのルールに従うことが効果的であり、正しい結果が得られます。確かに、コンピューターサイエンスは数学の一分野です。しかし、私たちが普段実践しているのはソフトウェアエンジニアリングであり、非常に独特なものです。私たちは、時間の制約とビジネス ニーズを考慮しながら、コンピューター サイエンスの成果と発見を実践に適用します。このブログは、コンピュータ プログラムの設計と実装に半数学的推論を適用する試みです。多くのプログラミング エラーを回避するためのフレームワークを提供する、さまざまな実行体制のモデルを提案します。
私たちがプログラミングを学び、最初の暫定的な (または大胆な) ステップを実行するとき、通常は単純なものから始めます。
私たちは筋肉の記憶を獲得し、言語の構文を学び、そして最も重要なことに、私たちは思考と推論の方法を変えます。私たちはコードを読み、コードがどのように実行されるかを推測することを学びます。私たちが言語標準を読むことから始めて、その「メモリ モデル」セクションを注意深く読むことはほとんどありません。それは、言語標準を十分に理解して活用するための準備がまだ整っていないからです。私たちは試行錯誤を繰り返します。最初のプログラムでは論理的および算術的なバグが発生します。これらの間違いは、私たちの仮定を確認することを教えてくれます。このループの不変式は正しいですか、この方法で配列要素のインデックスと長さを比較できますか (これはどこに -1 を置きますか)。しかし、何らかのエラーが見つからない場合、多くの場合、暗黙のうちに何らかのエラーが内部化されます。
つまり、これは次のとおりです。
コード行は常に同じ順序で (シリアル化されて) 評価されます。
この公準により、次の命題が真であると仮定することができます (証明するつもりはありません)。
数学的公理を使用すると、強固な基盤に基づいてより大きな構造を導出し、構築することができます。数学では、4+1 の公準をもつユークリッド幾何学があります。最後はこう言っています。
平行線は平行のままで、交差したり分岐したりしません。
何千年もの間、数学者はそれを証明し、最初の 4 つからそれを導き出そうとしました。それは不可能であることがわかりました。この「平行線」公準を代替案に置き換えて、さまざまな種類の幾何学形状 (つまり、双曲線や楕円形) を得ることができます。これにより、新たな可能性が開かれ、適用可能で有用であることがわかります。結局のところ、私たちの惑星の表面は平らではなく、GPS ソフトウェアや飛行機のルートなどでそれを考慮する必要があります。
しかしその前に、立ち止まってエンジニアリングに関する最も重要な質問をしてみましょう。なぜわざわざそうするのでしょうか?プログラムがその役割を果たし、サポート、維持、進化するのが簡単であれば、そもそも、なぜこの予測可能な逐次実行という居心地の良い不変条件を放棄する必要があるのでしょうか?
答えは 2 つあります。 1つ目はパフォーマンスです。プログラムを 2 倍の速度で実行できる場合、またはハードウェアの半分を必要とする場合、これはエンジニアリングの成果です。同じ量の計算リソースを使用する場合、2 倍 (または 3、4、5、10 倍) のデータを処理できます。これにより、同じプログラムのまったく新しいアプリケーションが開かれる可能性があります。サーバーではなく、ポケットの携帯電話上で実行される場合があります。場合によっては、賢いアルゴリズムを適用したり、よりパフォーマンスの高い言語で書き直すことによって速度を向上させることができます。はい、これらが最初に検討すべきオプションです。しかし、それらには限界があります。ほとんどの場合、アーキテクチャは実装よりも優れています。ムーアの法則は最近あまりうまくいっておらず、単一の CPU のパフォーマンスはゆっくりと成長しており、RAM のパフォーマンス (主にレイテンシー) が遅れています。したがって、当然のことながら、エンジニアは他の選択肢を探し始めました。
次に考慮すべき点は信頼性です。自然は混沌であり、熱力学の第 2 法則は、正確で連続的で反復可能なものに対して常に作用します。ビットが反転し、材料が劣化し、電力が低下し、ワイヤーが切れてプログラムの実行が妨げられます。逐次的かつ反復可能な抽象化を維持するのは困難な仕事になります。私たちのプログラムがソフトウェアやハードウェアの障害を乗り越えて存続できれば、ビジネス上の競争上の優位性を持つサービスを提供できます。これは、私たちが取り組み始めることができるもう 1 つのエンジニアリング課題です。
目標を設定したので、シリアル化されていないアプローチで実験を開始できます。
この疑似コードの部分を見てみましょう。
```
def fetch_coordinates(poi: str) -> Point:
…
def find_pois(center: Point, distance: int) -> List[str]:
…
def get_my_location() -> Point:
…
def fetch_coordinates(p) - Point:
…
def main():
me = get_my_location()
for point in find_pois(me, 500):
loc = fetch_coordinates(point)
sys.stdout.write(f“Name: {point} is at x={loc.x} y={loc.y}”)
コードを上から下まで読むと、「get_my_location」の後に「find_pois」関数が呼び出されることが合理的に想定できます。そして、次の POI を取得した後、最初の POI の座標を取得して返します。これらの仮定は正しく、メンタル モデルを構築し、プログラムについて推論することができます。
コードを非順次に実行できると想像してみましょう。これを構文的に行う方法はたくさんあります。ステートメントの並べ替えに関する実験 (これは最新のコンパイラや CPU が行うことです) をスキップし、新しい関数実行体制を表現できるように言語を拡張します。
スレッドには、POSIX スレッド、グリーン スレッド、コルーチン、ゴルーチンなど、さまざまな種類があります。詳細は大きく異なりますが、要するに実行できるものです。複数の関数を同時に実行できる場合は、それぞれに独自のスケジューリング ユニットが必要です。つまり、マルチスレッドは、実行スレッドが 1 つではなく、複数あることに由来しています。一部の環境 (MPI) や言語では暗黙的にスレッドを作成できますが、通常は、C の `pthread_create`、Python の `threading` モジュール クラス、または Go の単純な `go` ステートメントを使用して明示的にこれを行う必要があります。いくつかの予防措置を講じれば、同じコードをほぼ並行して実行できます。
def fetch_coordinates(poi, results, idx) -> None: … results[idx] = poi def main(): me = get_my_location() points = find_pois(me, 500) results = [None] * len(points) # Reserve space for each result threads = [] for i, point in enumerate(find_pois(me, 500)): # i - index for result thr = threading.Thread(target=fetch_coordinates, args=(poi, results, i)) thr.start() threads.append(thr) for thr in threads: thr.wait() for point, result in zip(points, results): sys.stdout.write(f“Name: {poi} is at x={loc.x} y={loc.y}”)
私たちはパフォーマンス目標を達成しました。プログラムは複数の CPU で実行でき、コア数の増加に応じて拡張でき、より速く終了します。私たちが次に問わなければならないエンジニアリングの質問は、どれくらいのコストがかかるのかということです。
私たちは、シリアル化された予測可能な実行を意図的に放棄しました。がある
次の結果は、今回はある関数が別の関数より先に終了する可能性があり、次回は別の方法で終了する可能性があるということです。この新しい実行体制はデータ競合を引き起こします。並行関数がデータを処理する場合、データに適用される操作の順序が未定義であることを意味します。私たちはデータ競合に遭遇し始め、以下を使用してそれに対処する方法を学びます。
この時点で、少なくとも 2 つのことがわかります。まず、データにアクセスする方法は複数あります。一部のデータは
この推論を続けると、スレッドローカル ストレージなどの他のテクニックが自然に登場します。私たちはプログラミング ツールベルトに新しいガジェットを追加し、ソフトウェアを構築することで達成できることを拡大しました。
ただし、まだ信頼できる不変条件があります。スレッドから共有 (リモート) データにアクセスすると、常にそれを取得します。一部のメモリ チャンクが使用できない状況は発生しません。バッキング物理メモリ領域に障害が発生した場合、OS はプロセスを強制終了してすべての参加者(スレッド) を終了します。ミューテックスをロックした場合、同じことが「私たちの」スレッドにも当てはまります。ロックを失う可能性はなく、実行中の作業を直ちに停止する必要があります。この不変条件 (OS と最新のハードウェアによって強制される) を利用して、すべての参加者は死亡しているか生存しているかのどちらかであると判断できます。すべては運命を共有しています。プロセス (OOM)、OS (カーネル バグ)、またはハードウェアに問題が発生した場合、すべてのスレッドは外部に副作用を残さずに一緒に存在しなくなります。
注意すべき重要な点が 1 つあります。スレッドを導入するという最初のステップをどのようにして行ったのでしょうか?私たちは別れ、分岐しました。 1 つのスケジューリング ユニットの代わりに、複数のスケジューリング ユニットを導入しました。この非共有アプローチを適用し続けて、どうなるか見てみましょう。今回はプロセスの仮想メモリをコピーします。これは、プロセスの生成と呼ばれます。プログラムの別のインスタンスを実行したり、他の既存のユーティリティを開始したりできます。これは以下に対する優れたアプローチです。
ほぼすべて ==
これは、運命共同体の不変性を放棄し、仮想メモリの共有を解除してコピーを作成することで発見した、さらに別の実行体制です。コピーは無料ではありません:
なぜここで止まるのですか?他に何をコピーしてプログラムを配布できるかを調べてみましょう。しかし、そもそもなぜ分散するのでしょうか?多くの場合、当面のタスクは 1 台のマシンを使用して解決できます。
分散化する必要がある
いくつか例を挙げると、
OS のアップグレード: 時々、マシンを再起動する必要があります。
ハードウェア障害: 予想よりも頻繁に発生します
外部障害: 停電やネットワークの停止は問題です。
OS をコピーすると、それを仮想マシンと呼び、顧客のプログラムを物理マシン上で実行し、その上に巨大なクラウド ビジネスを構築できます。 2 台以上のコンピュータを使用し、それぞれでプログラムを実行すると、プログラムはハードウェア障害が発生した場合でも存続し、24 時間年中無休のサービスを提供し、競争上の優位性を得ることができます。大企業ははるか昔にさらに進化し、現在ではインターネット大手が異なるデータセンターや大陸にさえコピーを実行しているため、台風や単純な停電に対してプログラムの回復力が強化されています。
しかし、この独立性には代償が伴います。古い不変条件は強制されず、私たちは独自に行動します。心配しないでください、私たちが初めてではありません。私たちを助けるテクニック、ツール、サービスはたくさんあります。
私たちは、システムとそのそれぞれの実行体制について推論する能力を獲得したところです。すべての大規模スケールアウト システムの内部では、ほとんどの部分はよく知られたシーケンシャルおよびステートレスであり、多くのコンポーネントはメモリ タイプと階層を備えたマルチスレッドであり、すべて真に分散された部分の組み合わせによってまとめられています。
目標は、現在どこにいるのか、どのような不変条件が保持されているのかを区別し、それに応じて動作 (変更/設計) できるようにすることです。 「未知の未知のもの」を「既知の未知のもの」に変換するという基本的な推論を強調しました。これは重要な進歩ですので、軽く考えないでください。