美しいコードを書いてプロジェクトをあふれさせる方法

「私たちは強力な魔法のインデックスを持つゾーンにさまよいました」と彼は説明しました。

「正確に」通り過ぎる茂みが答えた。

テリー・プラチェット、魔法の色




いコードを維持するのは不快です。 いコードは理解が難しく、多くの場合時代遅れであり、多くの場合エラーが含まれています。 しかし、これは正直な迷惑です-コードが大丈夫ではないことをすぐに知って、変更前に追加のテストを書いて、それを数回チェックし、すべてを修正するための見積もり時間を入れてください。



この点で美しいコードは異なります。簡単に読むことができ、通常は新しいテクノロジーを使用しており、最適に機能し、エラーがないと喜んで信じています。 これは単なる嘘かもしれませんが。







この記事では、コードを信頼できない(誰もが嘘をついている)ことを示し、いくつかの興味深いエラーを示します。





グアバ:より少ないコードを記述し、より多くのリソースを使う方法



Guavaは基本的なメソッドとオブジェクトのライブラリであり、標準ライブラリの代替としてGoogleによって作成され、多くの便利な機能を備えています。 特に、グアバはコレクションを操作するためのライブラリを実装しているため、データを簡単に操作できます。 この物語は、この単純さが悪に非常に使いやすいことを示しています。



主にデータ変換に関与するサービスを実装する必要があります。外部システムにデータを要求し、別の外部システムに通知し、要求元にデータを提供します。 実装は次のようになります。



private final static Predicate<Entity> checkPredicate = entity -> entity.performCheck(); private final static Function<Entity, Integer> mapToIdFunction = entity -> entity.getId(); private final Function<Integer, Entity> lookupFunction = id -> storageService.lookupEntity(id); @Override public ImmutableList<Entity> getCheckedEntityByIds(List<Integer> ids) { logger.info("Received " + ids.size() + " ids."); List<Entity> uncheckedEntities = Lists.transform(ids, lookupFunction); Collection<Entity> filteredEntities = Collections2.filter(uncheckedEntities, checkPredicate); notificationService.sendUpdate(Collections2.transform(filteredEntities, mapToIdFunction)); logger.info("Got " + filteredEntities.size() + " entities."); return ImmutableList.copyOf(filteredEntities); }
      
      







同意し、十分に簡潔に書かれており、ソースコードを読むだけで何が起こっているかを簡単に理解できます。 問題は1つだけです。この簡潔で簡潔なコードには、リソースの無意味な浪費につながり、データの内部エラーにつながる可能性のあるエラーが含まれています。 このテストでは、エラーを簡単に説明できます。



  @Before public void setup() { storageService = new StorageService() { @Override public Entity lookupEntity(int id) { lookupCounter++; return new Entity(id); } }; notificationService = new NotificationService() { @Override public void sendUpdate(Collection<Integer> ids) { System.out.println(ids); } }; guavaServiceImpl = new GuavaServiceImpl(storageService, notificationService); } @Test public void testGuava() { List<Integer> ids = new ArrayList<>(); for (int i = 0; i < 100; ++i) { ids.add(i); } guavaServiceImpl.getCheckedEntityByIds(ids); Assert.assertEquals(100, lookupCounter); }
      
      





java.lang.AssertionError:予想:<100>でした:<300>

org.junit.Assert.fail(Assert.java:88)で


ご覧のとおり、テスト結果は期待はずれです。合理的に可能な限り3倍のリクエストを行っています。 これは、対応するメソッドが実際に\ filter \などを変換するのではなく、対応するラッパーを作成するという事実によるものです。 つまり たとえば、コレクションの長さなどの情報を取得するには、上位コレクションのすべての要素を再確認(変換)する必要があります。これにより、リクエストするたびに要素が変換されます。



そのため、 最良の場合、単純な操作の隠れたコストによりパフォーマンスが低下します〜O(n * m)、ここでnは要素の数、mはコレクションのネストです。最悪の場合、これはメモリリークと他のサービスへの制御されない要求につながりますなぜなら 複数の要素を含むフィルターされたコレクションでさえ、すべての元の要素へのリンクを保存します。



グアバの使用に関するアドバイス:変換とフィルタリングの結果がコレクションのように見え、コレクションのように見せかけたとしても、それらをコレクションと見なすべきではなく、使い捨てであり、ほとんど使用する前にそのコンテンツを別のコレクションにコピーする必要があります。 たとえば、ImmutableList.copyOf()を使用することをお勧めします-これにより、他の問題から保護できます。



Google App Engine:ドキュメントを信じないことが役立つ理由

誰もが嘘をつきます。

ハウスドクター




Google App Engineは、クラウドアプリケーションを開発および展開するための非常に便利なプラットフォームであり、高負荷下でリソースを自動的に割り当ておよび配布し、データベースやファイルストレージなどのさまざまなサービスと迅速に連携する機能を備えています。 このストーリーでは、さまざまなコンポーネント間の統合が容易であることの落とし穴を示します。



このストーリーの主人公は、Hibernateを使用してビジネスエンティティにアップロードした1つのCloud SQLデータベース(Maria DB)を備えたApp Engineに基づく中規模プロジェクトです。



プロジェクトは発展しており、基盤は拡大していましたが、ある時点で、私たちの将来の患者は単に倒れました。 この秋には、ログの無効化とデータベースへの膨大な数の接続が伴いました(アプリケーションの数インスタンスのみ)。 また、いくつかの失敗したバックアップがデータベースログに見つかりました。 データベースを再起動した後、アプリケーションは何も起こらなかったかのように動作し始めました。



最も明らかな診断はループスであり、誰かがデータベースをブロックしています。 ログに長いライトスルートランザクションが見つからなかったため、データベースバックアップが原因と見なされ、ロックなしで動作するように再構成されました。 繰り返しの場合にトランザクションを見つけることができるように、バイナリログも含まれていました。



mysql binlogに関する叙情的な余談
MySQLはいくつかのタイプのログをサポートしています。 データベースを変更するトランザクションのみがバイナリログに記録されるため、本番サーバーで使用する場合でも十分に高速に動作します。

有効にするには、属性--log-bin [= base_name]でインスタンスを実行する必要があります。

次のリクエストを使用して、バイナリログが書き込まれていることを確認できます。
 SHOW VARIABLES LIKE 'log_bin'; SHOW BINARY LOGS;
      
      





mysqlbinlogユーティリティを使用して結果を読むことができます。




ただし、処理も分析も役に立たなかった-落下が繰り返され、バイナリログには疑わしいトランザクションは示されませんでした。



データベースに関連するすべてを徹底的に再確認した後、クラッシュの理由が見つかりました。これは、App Engineのドキュメントの推奨事項に従っていることが原因です。 特に、アプリケーションとApp Engine上のデータベースの場合、ドキュメントでは、新しいリクエストを受信するたびにデータベースへの新しい接続を開くようにアドバイスしています。 新しい接続を開くことは、古い接続を維持するよりも安価です。

データベース接続を最適に管理する方法は、ユースケースによって異なります。 たとえば、新しいデータベース接続を作成する時間が、既存の接続を確認して再利用するよりも長い場合は、接続プーリングを使用することをお勧めします。 逆に、新しい接続を作成する時間が、既存の接続が生きているかどうかをテストして再利用するのとほぼ同じ場合、 各HTTPリクエストを処理する新しい接続を作成し、リクエストの期間中それを再利用することをお勧めします。 特に、後者の場合は、Google App EngineからGoogle Cloud SQLに接続するときに適用される場合があります。

Google Cloud SQLのよくある質問


ただし、Hibernateと組み合わせて、このアプローチはかなり悲しい結果をもたらしました。このアプリケーションでは、テーブルからデータを選択するためのかなり重いクエリがいくつかあり、通常は正常に機能します。 ただし、複数の同時リクエストの場合、1分以上かかることがあり、App Engineによるリクエストの自動終了につながります。 Java側のクエリ結果を待機しているコードは実行を停止しますが、データベース側のセッションは結果の処理を続行し、次のクエリの速度を低下させ、一種の「雪だるま」をもたらします。



完全を期すために、接続プールの設定のおかげですべてが正常に終了したことを伝える価値があります。 Hibernateの場合、これはプロパティを置き換えることで実行できます
 <connection.pool_size>0</connection.pool_size>
      
      



接続プールの特定の実装を構成します。 App Engineの場合、推奨されるオプションはDBCPとHikariCPです。

警告:Hibernateのビルトイン接続プールは使用しないでください!
Hibernateのドキュメントには、テスト目的でのみ使用できることが明記されています。

ただし、Hibernate独自の接続プーリングアルゴリズムは非常に初歩的なものです。 開始を支援することを目的としており、実稼働システムでの使用、またはパフォーマンステストを目的とするものではありません。 最高のパフォーマンスと安定性を得るには、サードパーティのプールを使用する必要があります。 hibernate.connection.pool_sizeプロパティを接続プール固有の設定に置き換えるだけです。 これにより、Hibernateの内部プールがオフになります。 たとえば、c3p0を使用する場合があります。

Hibernate APIドキュメント




少し結論



これらの2つのストーリーは、コードの単純さと美しさが無料で提供されないことを象徴しています。 覚えておいてください-誰もが嘘をついているので、あなたのアプリケーションが思い通りに動作するためには、あなたの美しいコードとそれが実際に使用する魔法が何をするかを正確に理解する必要があります。



All Articles