- 投稿日:2022-02-28T23:09:09+09:00
RDSが「Storage-full」になった時の対応方法
概要 急にサイトが500エラーを発生してつながらなくなったので調査して見ると RDSのステータスが「Storage-full」、つまりデータ容量がいっぱいになったことが原因でつながらなくなっていた。 対処方法 前提として、「Storage-full」になるとインスタンスが停止するので、DBに繋げてデータを削除することができない。 そのためRDSのストレージ容量を拡張するをする必要がある。 以下、手順です。 1.RDSの画面から拡張したいDBを選択した状態で「変更」ボタンを押す 2.ストレージを増やす(例だと20GBから30GBに増やしている) ↓↓↓ 拡張する時の注意 RDSのストレージ容量を増やすと後から減らすことはできない。 どうしても減らしたい場合は、一度拡張した後にデータを削除した後にdumpを取ってから別のRDSインスタンスを作る必要がある。 ちなみに10GB増やすだけならそんなに金額は掛からない。 例: リージョン:東京リージョン ストレージタイプ:汎用 (SSD) ストレージ $0.138/GB × 10GBとなるので、$1.38/月となる。 1ドル=115円と計算すると約160円くらい。 参考:RDS料金 今後の対策 取れる方法は2つ 1.CloudWatchでストレージ容量を監視し、容量がいっぱいになったら消す 不要なデータを消すことができるならこれが一番お金がかからない。 2.RDSのオプションで容量をスケーリングさせる RDSのオプションで容量を自動拡張させることができる そもそもデータ消すことができないのであれば、こっちで対応した方がいい。 私の場合は、コストの関係上「1」で対応することになったが、CloudWatchなどの設定はそんなに難しくなく数分で対応できた。 CloudWatch設定方法はこちら 以上となります。
- 投稿日:2022-02-28T18:25:33+09:00
SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: Name or service not known の対処法
エラー内容 ./vender/bin/sail artisan migrate を実行すると、なぜか SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: Name or service not known こんなエラーが出ちゃいました。 前提として、 このコマンドの前に、色々キャッシュクリアなどやっていたら バグって、ページが表示されなくなるエラーを起こしていました。 そこで、新たなlaravelを立ち上げてmigrateしようと思っていた時に このエラーが起きました。 原因 前状況を考えると、キャッシュクリアなどで データベースが接続されていないのかな?と思い ./vender/bin/sail mysql match-app1 とDBを確認しようとしました。 すると service "mysql" is not running container と、違うエラーが発生したので。これは データベースが接続できていないのかも。 と思い、色々dockerとDBを繋ぐ方法を実行してみたものの 上手くいかず。。。 結局これで解決した あまりにも解決できなかったので、病みそうになっていたところ 適当に docker-compose up -d と打ち込んでみました。 それまでは ./vender/bin/sail up -d でやっていたのですが。 そして、dockerを立ち上げてから ./vender/bin/sail artisan migrate を実行してみたら、なんと上手くいきました!! Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (61.42ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (43.90ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (45.03ms) Migrating: 2019_12_14_000001_create_personal_access_tokens_table Migrated: 2019_12_14_000001_create_personal_access_tokens_table (70.68ms) Migrating: 2022_02_28_155100_create_user_lists_table Migrated: 2022_02_28_155100_create_user_lists_table (24.82ms) 久しぶりに、この色取り取りコードが見れてよかったです。 踏み込んで考えてみる さて、このままよかったと終わってもいいのですが なぜ、docker-compose up -dだと上手くいって ./vender/bin/sailだと、エラーが起きてしまったのかについて考えてみます。 ここを理解するには、そもそも composeとvenderの違いについて知る必要がありそうです。 sailスクリプトは、docker-compose.ymlファイルで定義されたDockerコンテナを操作するための便利なメソッドをCLIで提供します。 公式によると、sailコマンドはymlファイル操作用ぽいですね。 すみません。ちょっと時間かかりそうなので また、追々理解していくとします! とりあえず、今は終わらせることに集中していきます。
- 投稿日:2022-02-28T16:23:29+09:00
mysql5.7での所有権エラーを直そう!!
rails でmysqlを使ったらエラー出た。。 Mysql2::Error::ConnectionError (Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)): ERROR! The server quit without updating PID file (/usr/local/var/mysql/**.local.pid) 環境 MySQL : 5.7.37 macOS : 10.14.6 ruby : 2.7.2 rails : 6.0.3 調べると、所有権に問題があるというので、ログインするため下のコマンドを試してみました。 まずは、ログイン mysql -u root -p パスワードを何度打っても通らなかったので、パスワードがわからなくなったか、何らかの変更が加わったと思ったので、 mysql5.7を入れ直すことにしました。 まず入れ直すときに、大事なのはもともとあるファイルをしっかり削除することです。入れ直す場合、前のファイルが残っていると、エラーを引き起こし、スムーズにいかなくなる可能性があります。 ※削除すると,DBの情報が削除されるので、大事な情報を入れている場合は気おつけてください。 ファイル削除 sudo rm -rf /usr/local/var/mysql sudo rm -rf /usr/local/Cellar/mysql* sudo rm -rf /usr/local/bin/mysql* sudo rm -rf /usr/local/var/mysql* sudo rm -rf /usr/local/etc/my.cnf sudo rm -rf /usr/local/share/mysql* sudo rm -rf /usr/local/opt/mysql* sudo rm -rf /etc/my.cnf mysql.sock mysql.sock.lock も削除する。 cd /tmp ls # ファイル確認。 sudo rm -rf mysql.sock mysql.sock.lock プロセスを削除してください。 ps aux | grep mysql # 確認 kill -9 ???? # 権限を消す プロセスは、自分が実行したプログラムのことです。 mysql@5.7 アンインストールする。 brew uninstall mysql@5.7 アンインストールされているか心配ならコマンドで確かめてみてください。 mysql@5.7 インストールする。 brew install mysql@5.7 which mysql #mysqlが入っているか確認 インストールした後パスというのを通さないと、スムーズに進まないので、しっかりとコマンドを打っておきましょう!! パスを通す。 echo 'export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"' >> ~/.bash_profile パスとは、、 macの中のメモ帳に書いておいて、いつでも取り出せるようにしておくということ。。 パスを有効にするコマンド source ~/.bash_profile ログイン mysql -u root -p ログインして、パスワードをしっかりと変更する。変更するコマンドはこれです。 パスワード変更 UPDATE user SET authentication_string=password('新しいパスワード') WHERE user='root'; Query OK, 1 row affected, 1 warning (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 1 変更を反映 flush privileges; Query OK, 0 rows affected (0.00 sec) データベースを作る。 rails db:create めちゃめちゃ大変だったと思いますが、これで完成です!! ローカルサーバーを立ち上げる。 rails s 特にエラーの内容がよくわからない場合、結構さまようと思いますが、落ち着いて検索し、メモしつつ整理していけば、解決できることが多いので、落ち着いて頑張ってください!! お世話になった記事
- 投稿日:2022-02-28T15:36:50+09:00
MySQLのコマンド
コマンド MySQLコマンド 内容 mysql -u ユーザ -p MySQLへログイン(パスワードは後から入力) mysql -u ユーザ -pパスワード MySQLへログイン mysqldump -u ユーザ -pパスワード DB名 > ダンプファイル名 DBのダンプ BEGIN トランザクション開始 ROLLBACK トランザクション中の修正をロールバック COMMIT トランザクション中の修正を反映 EXPLAIN 確認したいSQL 実行計画の確認 show index from インデックスを確認したいテーブル名 テーブルのインデックスを確認 create index インデックス名 ON 対象のテーブル (対象のカラムリスト(カンマ区切り)) インデックスを貼る drop index インデックス名 ON 対象のテーブル インデックスを削除 参考
- 投稿日:2022-02-28T08:05:49+09:00
Springのバッチ処理をMySQLで排他制御する
複数サーバで稼働するSpringBootアプリケーションのバッチ処理をMySQLで排他制御したので備忘録として残します。 目的・前提 複数のサーバ上で同じSpringBootアプリケーションを稼働する。 バッチ処理でDBのデータを操作するため排他制御する必要がある。 バッチ処理は @crontab で起動する。 ライブラリ等 spring-boot-starter: 2.6.4 mybatis-spring-boot-starter: 2.2.2 lombok: 1.18.22 結論 MySQLのロック関数で排他制御が可能。 ただしバッチ中にMySQLのセッションが変わることがあるので、 @Transactional で同一セッションであることを明示する。 実装 ロック関数を実行するRepository, Serviceを作成 get_lock, release_lock 等をするServiceを作成する。 /src/main/java/{name-space}/repository/LockRepository.java @Repository public interface LockRepository { Integer isFreeLock(@Param("str") String str); Integer getLock(@Param("str") String str, @Param("timeout") int timeout); Integer releaseLock(@Param("str") String str); } /src/main/resource/mapper/LockRepository.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="{package-name}.repository.LockRepository"> <select id="isFreeLock" resultType="Integer"> select is_free_lock(#{str}) </select> <select id="getLock" resultType="Integer"> select get_lock(#{str}, #{timeout}) </select> <select id="releaseLock" resultType="Integer"> select release_lock(#{str}) </select> </mapper> /src/main/java/{package-name}/service/LockService.java @Service @RequiredArgsConstructor public class LockService { public enum LockResult { SUCCESS, FAILURE } private final LockRepository lockRepository; public boolean isFreeLock(String str) { Integer result = lockRepository.isFreeLock(str); if (result == null) { throw new ApplicationException("Database error"); } return result == 1; } public LockResult getLock(String str, int timeout) { Integer result = lockRepository.getLock(str, timeout); if (result == null) { throw new ApplicationException("Database error"); } return result == 1 ? LockResult.SUCCESS : LockResult.FAILURE; } public void releaseLock(String str) { Integer result = lockRepository.releaseLock(str); if (result == null) { throw new ApplicationException("Unknown lock"); } if (result == 0) { throw new ApplicationException("Can not release lock"); } } } バッチを作成 /src/main/java/{package-name}/batch/HourlyBatch.java @Component @RequiredArgsConstructor public class HourlyBatch { private static final String LOCK_STR = "hourly_batch"; private static final int LOCK_TIMEOUT = 0; private final LockService lockService; @Scheduled(cron = "0 0 * * * *") @Transactional public void execute() { // ロック確認 try { LockResult lockResult = lockService.getLock(LOCK_STR, LOCK_TIMEOUT); if (lockResult == LockResult.FAILURE) { // 別プロセスがロック済み return; } } catch (Exception e) { // エラー処理 return; } try { // データベース操作 } finally { try { lockService.releaseLock(LOCK_STR); } catch (Exception e) { // エラー処理 } } } }
- 投稿日:2022-02-28T05:47:00+09:00
MyBatisでデータを暗号化/複合化する
SpringBoot+MyBatisでデータを暗号化/複合化したので備忘録として残します。 目的 エンティティの特定データを暗号化してDBに登録する。 暗号化/複合化の実行はMyBatisの機能で行う。 MyBatisのMapperはXMLを使用する。 ライブラリ等 spring-boot-starter: 2.6.4 mybatis-spring-boot-starter: 2.2.2 lombok: 1.18.22 実装 以下、パッケージ名は {package-name}と表記する。 データベースを作成 ID、名前、Eメールアドレスを持つユーザ情報。 名前とEメールアドレスを暗号化する。 /src/main/resource/schema.sql DROP TABLE IF EXISTS user; CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) DEFAULT NULL, `email` varchar(256) DEFAULT NULL, PRIMARY KEY (`id`) ); /src/main/java/{package-name}/entity/User.java @Data public class User { private int id; private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } } 暗号化ロジックを作成 サンプルとしてシーザー暗号を採用した。 シーザー暗号は、アルファベットを決まった値だけシフトして暗号化する。 例えば、シフト数2で Qiita を暗号化すると Tllwd になる。 /src/main/java/{package-name}/cipher/CaesarCipher.java @Component public class CaesarCipher { // シフトする値 @Value("${caesar-cipher.shift:10}") private int shiftValue; // シフト private String shift(String str, int s) { if (s < 0) { s += 26; } StringBuilder sb = new StringBuilder(); for (char ch : str.toCharArray()) { if (!Character.isAlphabetic(ch)) { sb.append(ch); continue; } int base = Character.isUpperCase(ch) ? 'A' : 'a'; char newCh = (char) ((ch + s - base) % 26 + base); sb.append(newCh); } return sb.toString(); } // 暗号化 public String encipher(String text) { if (text == null) { return null; } return shift(text, shiftValue); } // 複合化 public String decipher(String text) { if (text == null) { return null; } return shift(text, -shiftValue); } } TypeHandlerを作成 MyBatisのMapperでカラムに紐づけるためのTypeHandlerを作成する。 DBからデータを取得してエンティティに格納する際に実行される。 暗号化するデータ型ごとに作成する必要がある。 /src/main/java/{package-name}/typehandler/CaesarCipherStringType.java // DBに格納する際のDataTypeを定義 // AliasはMapper内で参照する名前 @Alias("CaesarCipherString") public class CaesarCipherStringType { } /src/main/java/{package-name}/typehandler/CaesarCipherStringType.java // 定義したDataTypeに紐づくTypeHandler @MappedTypes(CaesarCipherStringType.class) public class CaesarCipherStringTypeHandler extends BaseTypeHandler<String> { private final CaesarCipher caesarCipher; public CaesarCipherStringTypeHandler(CaesarCipher caesarCipher) { this.caesarCipher = caesarCipher; } // null以外の値をDBに保存(暗号化) @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { String str = caesarCipher.encipher(parameter); ps.setString(i, str); } // カラム名指定でDBから取得(複合化) @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { try { String str = rs.getString(columnName); if (str == null) { return null; } return caesarCipher.decipher(str); } catch (Exception e) { throw new SQLException(e); } } // カラム番号指定でDBから取得(複合化) @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { try { String str = rs.getString(columnIndex); if (str == null) { return null; } return caesarCipher.decipher(str); } catch (Exception e) { throw new SQLException(e); } } // カラム番号指定でDBから取得(複合化) @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { try { String str = cs.getString(columnIndex); if (str == null) { return null; } return caesarCipher.decipher(str); } catch (Exception e) { throw new SQLException(e); } } } /src/main/java/{package-name}/config/MyBatisConfig.java @Configuration @MapperScan("{package-name}.repository") @RequiredArgsConstructor public class MyBatisConfig { private final CaesarCipher caesarCipher; // MyBatisのTypeHandlerにCaesarCipherStringTypeHandlerを登録 @Bean ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration -> { configuration.getTypeHandlerRegistry().register(new CaesarCipherStringTypeHandler(caesarCipher)); }; } } /src/main/resources/application.yml mybatis: mapper-locations: classpath*:/mapper/*.xml type-aliases-package: {package-name}.typehandler MapperでTypeHandlerを紐付け 先ほど作成したTypeHandlerをMapperに紐づけて、暗号化/複合化する /src/main/java/{package-name}/repository/UserRepository.java @Repository public interface UserRepository { // 複合かして取得 User findById(int id); // 複合化せずに取得 User findByIdWithoutDecipher(int id); // 暗号化して登録 void insert(User user); } /src/main/resources/mapper/UserRepository.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="{name-space}.repository.UserRepository"> <resultMap type="{name-space}.model.User" id="user"> <id column="id" property="id" /> <result column="name" property="name" javaType="CaesarCipherString" /> <!-- javaTypeで複合化方式を指定 --> <result column="email" property="email" javaType="CaesarCipherString" /> </resultMap> <resultMap type="{name-space}.model.User" id="userWithoutDeCipher"> <id column="id" property="id" /> <result column="name" property="name" /> <result column="email" property="email" /> </resultMap> <select id="findById" resultMap="user"> select id, name, email from user where id = #{id} </select> <select id="findByIdWithoutDecipher" resultMap="userWithoutDeCipher"> select id, name, email from user where id = #{id} </select> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into user (id, name, email) values (#{id}, #{name, javaType=CaesarCipherString}, <!-- javaTypeで暗号化方式を指定 --> #{email, javaType=CaesarCipherString}) </insert> </mapper> テストで動作確認 /test/resources/db/user.xml <?xml version="1.0" encoding="UTF-8" ?> <dataset> <user id="1" name="QDPH" email="qdph@hadpsoh.frp" /> </dataset> /src/test/java/{name-space}/UserRepositoryTest.java @SpringBootTest @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) public class UserRepositoryTest { @Autowired private UserRepository userRepository; // 複合化して取得 @Test @DatabaseSetup("/db/user.xml") public void findById() { User user = userRepository.findById(1); assertThat(user.getId()).isEqualTo(1); assertThat(user.getName()).isEqualTo("NAME"); assertThat(user.getEmail()).isEqualTo("name@example.com"); } // 複合化せずに取得 @Test @DatabaseSetup("/db/user.xml") public void findByIdWithoutDecipher() { User user = userRepository.findByIdWithoutDecipher(1); assertThat(user.getId()).isEqualTo(1); assertThat(user.getName()).isEqualTo("QDPH"); assertThat(user.getEmail()).isEqualTo("qdph@hadpsoh.frp"); } // 暗号化して登録 @Test @DatabaseSetup("/db/user.xml") public void insert() { User user1 = new User("NAME", "name@example.com"); userRepository.insert(user1); User user2 = userRepository.findById(user1.getId()); assertThat(user2.getName()).isEqualTo("QDPH"); assertThat(user2.getEmail()).isEqualTo("qdph@hadpsoh.frp"); } } GitHub
- 投稿日:2022-02-28T00:05:22+09:00
RDS Proxy利用時のLAST_INSERT_IDの挙動
目的 コネクションプーリングをしても採番テーブル(MySQL)が正しく利用できるかを確認したい。 目的(詳細) まず、以下のように、採番テーブルからidを取得する仕組みがあるとする(採番テーブルについては こちらの記事 を参照) UPDATE table_name SET id = LAST_INSERT_ID(id + 1); SELECT LAST_INSERT_ID() AS id; ここで、UPDATE文とSELECT文の間に排他制御を掛ける必要はない。 理由は、MySQLの「LAST_INSERT_ID」はグローバルな値ではなく、connection 単位で定義されるためである。 公式ドキュメントより "The ID that was generated is maintained in the server on a per-connection basis. This means that the value returned by the function to a given client is the first AUTO_INCREMENT value generated for most recent statement affecting an AUTO_INCREMENT column by that client. This value cannot be affected by other clients, even if they generate AUTO_INCREMENT values of their own. This behavior ensures that each client can retrieve its own ID without concern for the activity of other clients, and without the need for locks or transactions." 要するに、別の connection でLAST_INSERT_IDが更新されても影響されることはないようになっている。 一方、RDS Proxyを使ってDBのコネクションプーリングをする予定がある。ここでいう connection がDBのコネクションのことを指しているのであれば、コネクションを共有した別のクライアントにLAST_INSERT_IDを上書きされてしまうことはないかと考えた。 そこで、実際の挙動を確認してみる。 試験 シナリオ 複数のMySQLクライアントを起動し、RDS Proxy経由でMySQLのライターインスタンスに接続 各クライアントで順にLAST_INSERT_IDを更新していく 各クライアントで順にLAST_INSERT_IDの値を取得 期待する挙動 各クライアントのLAST_INSERT_IDの値が異なる(連番になっている)こと。すなわち、手順2でクライアント自身が設定したときの値となっており、他のクライアントの更新の影響を受けないこと 期待しない挙動 各クライアントのLAST_INSERT_IDの値が全て一致していること。手順2で最後のクライアントが設定したときの値となっており、他のクライアントの更新の影響を受けていること 試験結果 期待する挙動となっている(LAST_INSERT_IDはクライアント毎に異なる)。 ▼6個のクライアントで順にLAST_INSERT_IDを更新していく様子 ▼6個のクライアントでLAST_INSERT_IDの値を取得し、それぞれの値が異なっている様子 結論 RDS Proxy(コネクションプーリング)利用時にLAST_INSERT_IDの値がDBコネクション間で共有されるということはなく、クライアントに固有であることがわかった。おそらく採番テーブルとして利用して問題ないと考えられる。 補足: モニタリング情報 RDS Proxyのメトリクス クライアント接続数>DB接続数となっている ClientConenctions: 0→6に増加(+6) DatabaseConnections: 3→4に増加(+1) RDS Proxyのログ クライアント〜Proxy〜MySQL接続確立時のRDS Proxyのログ コネクションプーリングをしている様子がわかる clientConnectionが6種類確立されているのに対して、dbConnectionはid=800703549, 3258076081の2種類が都度使い回されている