ひらめの日常

日常のメモをつらつらと

『技術者のためのテクニカルライティング入門講座』を読んだ

はじめに

『日本語スタイルガイド』を読みまして、さらにエンジニアとして具体的な文書作成術を知りたいと思い、読みました。

hiramekun.hatenablog.com

感想や特に印象に残った点について残しておきます。

まとめ&感想

『日本語スタイルガイド』を読んでいれば精読する必要はないと感じました。

その上でこの本で良いと思ったのは、最後に文書作成の具体例があり、どのように実践するかが書いてある点です。文書作成術を学んでいる時には、「それで、実際具体的にどう書くの?」となることが多いです。なので、自分がドキュメントを書く際の助けになるのではないでしょうか。

さらには、内容からのアプローチのみではなく、見た目や箇条書きの見せ方のコツにも触れています。『日本語スタイルガイド』とはまた違った、ちょっとしたコツで読みやすいドキュメントを書くtipsを得ることができたのは良かったです。

特に印象に残ったところ

  • 最も重要な内容を先に書く
  • 「など」が示している対象を明確にする
  • 補足や自分の意見を括弧付で書かない
    • 1分が長くなり、冗長で理解しづらい文になる
  • 行頭記号は黒の面積が大きいものを上位階層にする
    • ■●▼ のように
  • 操作と結果は明確に分けて書く

『学力と階層』を読んだ

はじめに

出身階層や家庭環境といった、今まで教育の分野では見落とされてきた観点から、教育格差に対して切り込んでいる書籍です。2012年発刊ではあるものの、内部で紹介されている論文は2000年前後のものがほとんどです。なので、特に教育法や学習指導要領周りは大きな変化があり、そのまま鵜呑みにできる内容ではありません。ただ、学力の階層差という視点から教育格差に切り込んでいくところに興味を持ち、読んでみました。

学力と階層 (朝日文庫)

学力と階層 (朝日文庫)

感想や特に印象に残ったところをまとめます。

まとめ&感想

学力の階層差は年々広がっているという一貫した調査結果が印象的でした。その各々の調査についても、平均値を取ったりするだけではなく相関係数を取ったり重回帰分析をして影響の大きいファクターを推定していました。

後半部分の教員の実態については、以前読んだこちらの記事の方が新しいため、こちらを参照しました。

hiramekun.hatenablog.com

さらには、「自分で考える力」など比較的「新しい学力」に取り組む姿勢に関しても、家庭の階層を反映しているということでした。家庭の階層差をどのように埋めていくのか、それに関して具体的な言及はなかったので、この先の自分の中のテーマとして残りそうです。

印象に残ったところ

授業の理解度、学習意欲に示される格差

「人的資本」の内容が、獲得される知識のストックから、知識獲得のためのスキルへと変わりつつあるといってもよい。そうした学習能力を核とした人的資本を「学習資本」と呼べば、現代はまさに学習資本主義の時代である。

21世紀型の経済社会では、知識技術は移り変わりが激しく、その知識技術の移り変わりに対応できる能力が必要とされる。この能力は詰め込まれた知識以上に大切であり、さらにはその知識を活用し、足りない知識を見つけ出して補完することが必要。ここでこの能力を学習資本と呼んでいる。

家庭の社会階層と、学習活動や態度の分析結果についてもまとめられている。まず、学校外での1日あたりの学習時間は階層差が生まれている。さらに、学習の理解度に関しても、次の図のように階層差が生まれている。

さらに、階層上位の子供ほど、「総合的な学習の時間」のような自ら主体的に学ぶ授業において学習のまとめ役になる。このことから、知識技術の習得のみならず、学習資本の形成にも大きな階層差が生じている

このような階層差を解消するためには、より階層下位の子供たちに資源配分をする必要がある。つまり、フェアな社会を作るならば、早すぎる段階で学習から降りてしまう子供が、特定の社会階層に偏るのは望ましくない。よって、これまで以上に、「下に手厚い支援が必要」と筆者は主張している。

家庭的背景が学力に大きな影響を及ぼす

ここでは、子どもの生まれ育つ家庭環境によって、学力にどのような差異が見られるのかを確認するとともに、そうした差異が、時代の変化の中で拡大しているのか、それとも縮小しているのかを、調査データを用いて検証する。「学力」の階層差を確認するだけではなく、階層差の変化に焦点を当てるところに、分析の特徴がある。果たして、学力の階層差は拡大しているのかどうか。

学校によって縮小されていた学力の階層差が、学校の役割が減ったことにより、より露呈しやすくなったのではないかと仮説を立てている。

全ての階層において勉強離れが進む中、基本的な生活習慣が身についていない家庭の子供の方が勉強離れが進んでいる。階層間の生活習慣の身につき度合いと算数の正答率の差は、年々広がっていることがわかる。つまり、基本的生活習慣が身についているかの影響が拡大しているということである。

次に、塾にもいかず、家でもほとんど勉強していない生徒(No Study Kids: NSKと略記されている)の増加と、それ起因で起こる学力の低下という問題に焦点を当てる。なぜかというと、NSKに注目することで、学校の授業のみの教育効果の変化を取り出すことができると考えられるため。

結果は、89年ではNo Studyの影響は統計的に有意ではない。しかし、01年になると、統計的に有意になり、大きな値を示すようになる。学校の授業だけに頼っている中学生の場合、01年はそのことで正答率が6点低下する

そして、こうなると「どのような家庭の子供がNo Studyになるのか?」という疑問が生じる。ここには、家庭の文化的環境や父親の学歴が影響している。家庭の文化的環境がNo Studyに影響を与えており、年々No Studyの子供とそれ以外の子供の格差は広がっている。このことから、筆者は次のように主張する。

教育改革をめぐる議論において、こうした家庭的背景の影響が無視され続けてきたが、ここでの結果は、それが無視できないものであることを示している。

学習時間の階層差とその拡大

高校生の学校外での学習時間に注目し、その変化と、変化に関わる要因の分析を行う。

元々学習時間は学力の代替指標と見られてきた。この本では特に努力の指標として捉える。なぜ努力の指標として注目するかというと、「努力は社会階層とどのような関係を持つのか?」を知りたいからである。
そして、なぜ「努力」なのかというと、個人の属性よりも能力と努力からなるメリットが重視される「メリトクラシー」と呼ばれる社会においては、能力と努力のいずれもが、個人の属性から影響を受けないことが、平等さを実現するために必要とされているからだ。

結論から言うと

  • 出身階層で努力の差は拡大している。
    • 79年の結果では、ランクの高い高校に入れば、学習時間の階層差が縮小することが発生していた。
    • しかし97年の結果では、高校のランクを取り除いたとしても、母親の学歴などの階層要因が独自の効果を学習時間に影響を及ぼすことが明らかになった。
  • 受験戦争の緩和が不平等を拡大している。
    • 受験戦争では皆を受験へと向かわせようとする圧力があったが、それが緩和されてきている。
    • これにより学校の受験への後押しが弱まると、個人個人がどのような学習をするか選択する必要が出てくる。そうして、努力の階層差が拡大する。

義務教育機会の不均衡化は経済格差を生む

財政力の弱い県ほど、一人あたりの教育費がかかり、国の義務教育を通じて資源の再分配が行われている。現状は、国の財政調整によって、各地方の義務教育の質と量を担保している状況。これにより、国が平等に機会を与えているという印象がつけられている。しかしながら、異なる地域間での学力格差の是正に、この財政調整がどれだけ効果があるのかは明らかになっていない
よって、地域間の学校の状態が均一であると印象づけることで、本当は存在する地域間の環境の差異を消してしまっている。

これからは、どこで義務教育を受けたかについて無視して扱うことは疑問が投げかけられるだろうと主張している。

地方の財政力の格差の背後に、地方の経済力の差、さらには、家計の経済力の差があることを思い起こせば、地域間の義務教育機会の不均等化は、社会経済的格差の拡大と結びつく可能性も高い

『日本語スタイルガイド』を読んだ

はじめに

ドキュメントやSlackの文章を書くときに、より相手に伝わるように書きたいというモチベーションから、『日本語スタイルガイド』を読みました。基本的な文章作成の指針について、テクニカルコミュニケーター協会が出している本になります。

日本語スタイルガイド(第3版)

日本語スタイルガイド(第3版)

感想や特に印象に残った点についてメモとして残しておきます。

まとめ&感想

「似ている助詞の注意点や語尾の使い分け」といった細かいけど大事な日本語の使い方に始まり、「文章全体をどのように構成していくか」その時の心構えまで説明されています。さらには「文章を校正する際に気をつける点」や「翻訳しやすい文章の書き方」まで書いてあり、広く浅くテクニカルライティングに必要な力をカバーできているのではないかと感じました。

実際に、仕事で文章を書く際に、この本の内容を思い出しながら、文章を書き直しできる機会が増えてきました。

個人的にはもう少し、「ドキュメントをいかにして書くか、どのような構成で書くか」といった点について勉強を深めたいので、『技術者のためのテクニカルライティング入門講座』を読みたいです。

特に印象に残ったところ

読みやすく書くポイント

「ですます調」と「である調」

  • 「ですます調」は、優しく柔らかい雰囲気で伝えることができる。また読み手に内容も平易であるかのような印象も与える。なので、読み手が「内容を理解できるかどうか」不安に感じている可能性がある場合は、「ですます調」を用いるのが良い。
  • 「である調」は、力強く自身のある雰囲気で伝えることができる。また読み手に内容も正確で厳密であるかのような印象を与えることができる。なので、内容の信頼性が重要な場合は「である調」を使うことがある。

混在させないのが基本だが、例えば操作内容は「である調」だが操作結果は「ですます調」で書くなど使い分けることもある。

能動態と受動態

操作説明を行う文章では、視点を統一するために、利用者を主語にして能動態で書く。それに対して、利用者から見て自動的に行われる操作は受動的に表現する。例えば、利用者が行なった操作の結果は受動態で書く。

時制を統一する

操作を示す文の時制は現在形で統一する。そうすることで、利用者は文章を読み進めながら、機器やソフトウェアをその時点で自らが動かしているような気持ちになる

1つの文は1つの事柄を書く

文を接続助詞(〜ので、〜が、〜と、など)で繋ぐと、複数の意味を持つ文になりやすいので注意する

1つの行動を1つの文で書く

1つの行動の説明には1つの文を使う。順番が決まっている場合は、連続した番号をつけると手順の数や順番が明確になる。

「〜します」、「〜してください」、「〜できます」を使い分ける

基本は「〜します」を使う。使用情報では、利用者に実行してほしいことは「〜してください」とし、基本操作を補足するときは「〜できます」と書き分ける。

  • 〜します:基本お操作手順。実行するかどうかを利用者の判断に任せる場合も同様。
  • 〜してください:操作手順で利用者にお願いする表現。(例:〜を確認してください)
  • 〜できます:操作手順で、「〜します」の文を補足するときに使う。

1文は50字以内を推奨する

1分の長さが50字を超える場合は、文を分けられないか検討する。また、名称や説明が複数並んで文が長くなっているときは、箇条書きするなどの工夫も必要になる。

「〜し、」、「〜り、」を使って文を長くしない

前後の関係が分かりにくくなることがあるので、文を分けると読みやすくなる。
また、2つの関係を明確にするためには「〜し、」で繋がずに表現を変える。例えば、「〜しながら」、「〜のまま」、のように。

必要な主語を省略しない

主語が利用者である場合や明確な場合を除いて、必要な主語を省略しないようにする。特に主語が省略された状態で、文の途中で主語を変えると意味が正しく伝わらなかったりする

主語と述語を近づける

主語と述語の間に長い語句が入ると意味がわかりにくくなるので、主語と述語を近づける。

複数の修飾語・修飾部がある場合は、長い修飾語・修飾部から順に記載する

文字数の多いものから並べると誤解されにくくなる。時を示す修飾語・修飾部は、長さに関係なく前に出す

読者への配慮を第一に

大事なのは読み手への配慮。相手の視点に立つことがテクニカルライティングにおいて最も重要な要素の一つ。

読み手の知識に配慮する

読み手が知っていることを土台にすることが重要。最初は読み手の知っていることから初めて、段階を追うように説明する。関連知識を持っていないと、全く理解できずに一歩目でつまずいてしまう。

専門用語に関しては、説明してから使う必要がある。もしくは、理解できるように説明しながら使う。本から引用すると、例として以下のような文章がある。

ソフトウェアを自分のコンピューターに組み込む「インストール」という作業が必要でした。

読み手の心情に配慮する

読み手が不安に感じる要素を取り除くことが大切。そのためにも、否定的な表現で無用な不安を与えることは避ける。

  • 「〇〇しかできません」→「〇〇できます」
  • 「(ポジティブ)ですが(ネガティブです)」→「(ネガティブ)ですが(ポジティブです)」

3ヶ月でやった事を振り返る - 入社1年目10~12月

はじめに

3ヶ月ごとに何をやったか、社内・社外関係なくブログにまとめていきたいと思います。今年度のこれまでの記事はこちら

hiramekun.hatenablog.com

hiramekun.hatenablog.com

お仕事

ScalaMatsuriに参加しました。「Scala界隈は怖い」といった話をどこかで見かけたのですがそんなことはなく、初学者にも優しい世界だと感じました。エンジニアの教育論などについても盛んに議論されていたり、初心者向けのセッションがあったりしたので、自分も楽しむことができました。(実は弊社もスポンサーでした..!!!)

scalamatsuri.org

教育に関連が深い理論である項目応答理論を超初心者向けに説明したりしてました。内容は本当に基礎の基礎ですが。 f:id:thescript1210:20201226220146p:plain:w500

ドメイン駆動設計(DDD)を取り入れたりしてました。正確には、元々プロジェクトにDDDの考え方は入っていたんだけど、自分がその知識を持っていなかったためにコードを読んでも設計思想に気づきませんでした。やってみて思ったのは、これまでなんとなくやってきたことに明確に名前がついて統一された設計があると非常に仕事がやりやすいし、無駄なところで悩まなくて済むので良いと思いました。

最近難しいと感じているのは、工数を見積もる能力です。エンジニアは納期を守るべきなので、自分の工数見積もりができなければいけないと思っています。自分の見積もった工数と、実際にかかった工数をメモしておいて、終わった後に工数見積もりに関して反省できるように仕組みを作っていきます。エンジアリングのコーディング以外の技術つながりでいうと、ドキュメンテーション能力が低いことも気になりました。抽象→具体の説明を心がけたり、友人がプレゼントしてくれた 日本語スタイルガイド(第3版) を読んでその内容を反映したいと思います。

Scala周りの勉強も、自分で進めるものが少し疎かになっていました。1月からはAkka実践バイブル アクターモデルによる並行・分散システムの実現sbt in Action: The simple Scala build toolを読んで、言語の周辺知識も深めています。

お勉強

業務に関係しそうなことと、統計検定をいつか取りたいなーという思いからその辺関係あるところを勉強してました。

わかる!ドメイン駆動設計〜もちこちゃんの大冒険〜

DDDの浅く外観を掴むことのできる本。この本の次にドメイン駆動設計入門を読んだのですが、大まかに言葉とその意味だけでも押さえておいたおかげで導入がスムーズだった気がします。さらに、物語調なのでシンプルに読んでいて面白いです笑

booth.pm

ドメイン駆動設計入門

いろいろなところでおすすめされている本をDDDの入門書として読みました。おすすめされているように、ボトムアップで具体例をみながらの説明になっており、非常にわかりやすく、他の方にもお勧めできる本でした。

StanとRでベイズ統計モデリング

ベイズ統計とMCMCなどのサンプリング手法は組み合わせて用いられると以前読んだ参考書にも書いてありましたが、それを具体的にStanで実装する本です。モデリングの流れの説明から、業務で用いる際に引っかかるであろう細かいポイントまで網羅されており、Stanを使う人には必須の本だと感じました。

統計のための行列代数 上

100ページくらい読みました。統計に特化した内容のため、普通の教科書でやることがかなり後ろに出てきたりします。証明が一つ一つあり、それをおうと大変ですが、説明は丁寧で追うことができます。一旦プログラミングのための線形代数を読み終わってから戻ってこようと思っています。

統計のための行列代数 上

統計のための行列代数 上

スタンフォードの自分を変える教室

自分のブログにまとめました。自分の行動を変えたいと思い、1日1つ意識するようにしているのですが、自分の目標に向かった行動ができていると感じます。(瞑想はいいぞ)

hiramekun.hatenablog.com

アメリカ海軍が実戦している「無敵の心」の作り方

会社の方に勧められて読んでみました。これも基本的には「瞑想はいいぞ」という話です。洋書にありがちな具体例の列挙が多く、経験談としてストーリーを楽しむのはよかったのものの、後半は少し読むのに疲れてしまいました。

図解人材マネジメント入門

その名の通り、人材マネジメントとして抑えておきたいポイントをたくさんの図で説明してある本です。評価や制度の関係性について知ることができました。(下図は本より引用したものです。)

趣味

スマブラ

スマメイト1600を達成できました。ただ、1500後半から1600前半をうろついているイメージがあったり、気を抜くと急に落ちたりするので、1600安定を次の目標として頑張っていきたいです。

カメラ

新しいカメラを買ってしまいました。自分はオリンパスが好きなので、カメラ事業から撤退すると知っていても購入しました。このようなご時世ですが、カメラを使って散歩中に写真を撮ったりして気分転換するのも良いですよ。

『子どもがつまずかない教師の教え方10の「原理・原則」』を読んだ

はじめに

本はこちら

子供がどこでつまずいているのかを分析しながら、理論と実践を結びつけることで、エビデンスベースの実践へと高めるための本です。特に優秀と言われる先生の中から、その人たちが実践している原理原則を、認知心理学の視点からまとめています。感想や印象に残ったところをまとめます。

まとめ&感想

理論と実践を結びつけるという本でしたが、個人的には理論についてもう少しつっこんだ記述が欲しかったです。ただ、今まで理論を勉強していても、それがどのように実践されるかについては触れることが少なかったので、橋渡しする本としては読んでいて面白かったです。理論の本は終始理論を語り、実践の本は終始実践をいかにするかを語っている印象があるのですが、その両方を行き来している本としては貴重ではないでしょうか。

特に、狙いとそれに向かうための手段を意識した授業作りが大切 という言葉が印象に残りました。「一斉授業だ」「協同学習だ」のように、学習方法だけに踊らされてしまいがちですが、どのような狙いがあって、どれに向かうためにどのような授業を作っていくかを考えていくのが大切だと感じました。

印象に残ったところ

第1章 学習者検証の原則

学習者検証の原則

「わかる」ことは「できる」の上位概念である。 つまり、少なくとも子供ができていないうちはわかっていないということである。これを教授理論では 学習者検証の原則 という。大事なことは、子供ができていないうちはわかっていないと捉えること。

できていない時は、そのアウトプットである誤答を丁寧に分析し、何をわかっていないのかを知ることが大事

ガニエの学習成果の5分類

授業には目当てや狙いがあり、学習成果の質的な差によって種類が分けられる。学習成果の5分類がそれに当たる。


(画像は本より引用)

手続き的知識・技能の取得と概念的理解

手続き的知識・技能はほぼ全ての子供が習得できる力。その一方で、概念的理解は全ての子供が習得できるとは限らない。

まずは手続き的知識・技能を身につけさせるべきであり、基本的な考え方は教師が教え、反復練習を中心に基礎的な力の定着を図る。

第2章 成長マインドセットを育む

伸びる子に共通するのは、どうしたらできるかを追求する姿勢。これを心理学では 成長マインドセット と呼ぶ。困難な課題に直面した時、やればできると考える傾向のこと。

これと反対なのが 固定マインドセット で、失敗は恥ずかしいと捉える傾向のこと。

子供の成長マインドセットを育てるためには、以下のようなことが大事。

子どもの成長マインドセットを育てるためには、教師自身が成長マインドセットをもつことが大切です。これは学習者検証の原則とも大いに関係があります。子ども達ができていないうちは、上手く教えることができていないと考えるのです。

第3章 「算数」で読解力を底上げする

読解力は数学・科学的リテラシーと関係している。また、算数の読解力は次の3つの項目で伸ばすことができる。

  • 精緻化:詳しい情報を付け加えること(時速xkmとは、1時間にxkm進むなど)
  • 要約:文章題で問題文が何を問いかけているかを読み取ること
  • 例示:わかりやすい数字に置き換えること(分数を一旦整数に置き換えて考えるなど)

文章題はただ読ませるのではなく、狙いをはっきりとさせて読み取ることが大事。

第4章 アウトプットを意識する

算数でのつまずきの3大要因は 計算力の欠如、読解力の欠如、公式や解法を思い出せないこと

記憶の3つのプロセス

記憶には3つのプロセスが存在する。

  • 記銘:覚える。頭の中へ情報を取り込むこと。
  • 保持:覚えている。長期記憶に保存された情報は基本的には消えないこと。
  • 想起:思い出す。長期記憶から情報を取り出すこと。

この中で 想起が一番大事なプロセス。繰り返し思い出すことを通して、すぐに思い出せるようになるのが覚えたという現象になる。

テスト効果

小テストをしたり問題を解いたりする方が学習効果が高いということ。これをテスト効果という。例題を解いた後に、累代を解くという問題演習型の授業は効果的。

復習の方法

  • 間違えた問題を中心にやった方が効率的
  • 何回も覚えた内容を勉強し直す分散学習が効果的。復習→忘れる→再復習の繰り返しが、学習内容をより強化する。

第5章 スモールステップで始める

まずはできることから始めるのが大事。数式の原理を覚えられない時は、まずは音読してみるのが良い。声に出して言えないものは覚えられないので、音読は覚える前に効果的。

第6章 ワーキングメモリーを節約する

ワーキングメモリーとは、例えば暗算を行うような情報操作を行う短期記憶の一つ。短期記憶の中でも高度な認知的活動のことをさす。ワーキングメモリーには限りがあるので、余計にワーキングメモリーを消費しないためにも、まずは読めるように音読したり、暗唱したりすることは大事。

第7章 視覚化してイメージで捉える

特に4年性以降での算数のつまずきの大半は、具体を抽象化できないこと、抽象を抽象で捉えられないこと。 具体的に視覚的にイメージできるようにすることは対策の一つとして有効。見た目の数値に捉われるあまり、具体的にイメージすることが難しくなっていることがある。

第8章 学習方略を身につける

守破離の精神で学習者に接する心構えが必要。この心構えを持つことで、次第に子供に自ら学ぶ姿勢や方法が身につく。教師が子供に例示する学習方法を通じて、学習方略を子供が身につけることが大切。

視覚化してイメージすることも、ほとんどの子供がそのように考える思考の習慣がついていない。ここでも教師が解き方を例示し、子供が実際に考えられるようになるまで根気強く指導することが必要。

第9章 メタ認知を促す

メタ認知とは

メタ認知とは、「わからない」ことが「わかる」ということ。多くの子供にそのようなメタ認知能力が初めから備わっているわけではないのに、ドリル学習や家庭学習によって勉強する内容ややり方を子供に任せるのはおかしな発想である。

ちなみに、自学ノートでは解き直しをさせるのが効果的。正答率が8割程度のものを解き直すのが最適とされている。

メタ認知には行動が伴う必要がある

メタ認知できるということは、新たに具体的な行動目標を立てられるということ。メタ認知を子供に促す際には、精神論や根性論で指導するのではなく、具体的な行動を支持することが必要になる。(例:メモしましょうと伝えるだけではなく、その場で実際にメモを取らせる。)

学習の振り返り時には、認知的な枠組みや行動を再確認することが必要。

  • 認知的枠組み:自分がどのような間違いをしていて、どうすればその間違いを修正できるかということ。
  • 行動様式:認知的枠組みを踏まえて、具体的にどのような学習行動を取れば良いのか振り返ること。

第10章 ピア・ラーニングを取り入れる

ピア効果とは

ピア・ラーニングとは、学び合いのような協同的な学習のこと。そもそも学力に影響を与える要因として、学級での児童生徒の行動が重要とわかっている。ピア効果とは、能力が高い集団に属していることで、お互いを高める効果が生まれるということ。

ピア・ラーニングが効果的な理由は、協同的な学習と個別的な学習を併用することが、個々の学習を促しているからである。

個別学習と協同学習

個別学習と協同学習は対極の概念ではない。学習成果という狙いがあり、それを達成するための手段として、指導・学習形態が様々に存在しているだけである。

協同学習がうまく機能するには、学習者の3つの要素が関係している

  • 類似性:年齢は語彙力などの認知レベルが近いこと
  • 互恵性:お互いのやりとりが双方の学びにつながること
  • 自発性:お互いのやりとりが自発的に発生すること

この目標を達成するには、具体的な手立てや問題解決の過程が対話によって仲間で共有される必要がある。こうすることで、学習者が迷うことなく、目標達成のために行動することができる。

学びの個別最適化

教師は原理原則を教えたうえで、協同学習を行う方が効果が高い。一斉授業+協同学習や個別学習のハイブリッド型授業は柔軟性が高く、どの子供にも伸び代がある授業設計。

授業で授業で対話を重視するあまり、読み書き計算のような反復練習が疎かになっていることも多い。やはり基礎基本は反復練習を中心とした学習を重視するべき。

  • 概念の理解や思考は、対話を中心とした協同学習で身につける
  • 手続き的理解や技能は、反復練習を中心とした学習で身につける

という、狙いとそれに向かうための手段を意識した授業作りが大切

『教師崩壊』を読んだ

はじめに

本はこちら

日本の教師が危機的な状態にあることを、データも交えながら論じ、警鐘を鳴らしている本です。感想や印象に残っているところ中心にまとめます。

まとめ&感想

冒頭では教員の危機的状況をまとめ論じています。後半ではそれを解決するために、先生の仕事を取捨選択していき、先生以外も今先生が負担している役割を担っていくべきだという主張でした。

自分が学生の頃、先生がどれだけ忙しいか考えてもみなかったでですが、第三者としてその働き方をみると危機感を覚えました。本の中でも言及されていますが、長時間労働が普通の状態になっていて、しかも残業代が出ないというところは根本的に変えていく必要がありそうです。

決まっていく政策や指導要領などをみると、要求だけが無限に増えている 印象を受けます。普通の会社であれば、要求が増えるのであれば人員を確保したり、待遇をあげたりということがあるのですが、教師に関してそのような議論になってるのを見たことがないですね。

正直細かいところは首を傾げる点も多かったですが、全体としては、データを元にして教育現場の危機的状況を伝えている本として読む価値がある本だと思いました。

印象に残ったところ

教師の危機的状況

過労死等のリスクが非常に高い状態で働いている先生は、小中学校で6~7割前後にも上る可能性が示されています。うつ病などで休職を余儀なくされている先生は毎年5000人もいます。実際、この10~20年、教師の過労死、過労自殺はあとを絶ちません

このような危機的な状況にある中で、教師や学校のあり方について、データやファクトに基づいて論じることを目的としている。(これまでは感情論や一部の経験に基づくイメージによって語られることが多かった。)

教師が足りない

まず、そもそも教師の数が足りていない現状がある。

2017年4月の始業式時点で、半数近い32の自治体で、定数(国の示す標準として配置されるべき数)に対して、少なくとも717人もの教員が不足していたことが明らかになりました。

なぜかという理由の一つには、教員採用試験の倍率低下が見られ、さらに深く調査すると、教員免許を取得しても採用試験を受けない人が多く存在する。

小学校では4割近く、中学校では6割以上、高校では8割以上が、教員免許を取得しても採用試験を受けない

倍率が下がっても優秀な人が先生になっていれば良いのだが、現実は違う。実際現場の先生たちも、最近優秀な人材が教員を目指さなくなっていると実感しているデータがあるらしい。
さらに、ベテランの先生たちも自身が忙しいこと、育成するべき人が多いことなどがあり、職場での育成は機能していない学校が増えている。

教師の質が危ない

日本の教育のワースト記録として、以下の二つが挙げられる。(ワーストと表現しているが、あくまでワーストクラス)。

  • 政府が教育にかける予算
  • 教員の労働時間の長さ

世界でも例を見ないほど少ない予算規模でありながらも、日本の子供達の数学や科学の学力がかなり高いのは、教員の長時間労働などによって支えられている部分もあるとされている。

教員向け調査においても、先生の悩み第一位は 「授業準備の時間がない」ことだと明らかになっている。このような状況で良い授業を行えというのも無理がある。

失われる先生の命

過労死ラインは週に60時間以上勤務だが、この過労死ラインを超えて働く人の割合は、特に小中学校では他業種と比べても突出して高い。(画像は本から引用)

  • 過労死だけに限った数ではないが、毎年400~500人の教員が死亡している。
  • 毎年5000人の精神疾患休職を出している。

なぜこんなに労働時間が長くなるかというと、生徒指導や部活指導などで子供と向き合ってきた結果長時間労働をしてしまっている現実がある。

学びを放棄する教師たち

データから見られる長時間労働のもたらしている最大の弊害とは、能力開発の機会損失である とされている。これは学校に関しても当てはまり、教職員の学びの余裕がなくなってきている

全国公立学校教頭会も、国の審議会の場で、本来は教頭は職場の人材育成に時間を割きたいが、そこに時間を割けていないことを訴えている。

自分でも学べないし、他の先生が育成する時間もない。これらのことから、OJTもOff-JTも不十分で育成される機会はほとんどないのが現状。

信頼されない教師たち

神戸市で教師間のいじめ事件が話題になった。この事件もきっかけとなり、教師不信や学校不信がかつてないほど高まったと本では述べられている。
神戸市の教育委員会では、女子中学生がいじめを苦に自殺した事件でも、証言メモを隠蔽した問題もある。

わいせつ行為で処分された教員の数は2018年度に過去最多を記録するなど、教師の不祥事について心配な点も多々ある。

教師崩壊を食い止めろ!

まずは学校の役割の肥大化を食い止めることに注目するべきだと主張している。

学校や教員の役割、業務を、いまよりもっと絞り込んで限定して、その真に大切な部分に時間とエネルギーと予算とを集中してつぎ込んでいくこと
(中略)
子ども一人ひとりに目をかけることを必要とする教育を求めておいて、そのための条件整備にはお金を出さない。時間的余裕も与えない。それでも「自ら学び、考える力」の教育が大切だというのは、欲ばり過ぎというほかない。

登下校や夜間の対応など、「子供のため」という大義名分のために学校の教員が行う仕事が増えてきてしまっている。教員以外ができる仕事に関しては他の人に任せることが必要なのではないか。

教員は特に朝と夜に勤務時間外の仕事が多いが、公立学校の場合は教員の残業代は出ていない(教職調整学で月給の4%が出ているだけ)。(画像は本より引用。)

f:id:thescript1210:20210121235911p:plain

文科省有識者、専門家それぞれが自分たちの立場から考えているため、トータルで学校がどうなるか、負担がどうなるかについて深く考えていない。その具体例として、全国学力テストやキャリア・パスポートなどが挙げられる。教師個人ではなく、学校組織全体や社会全体が一つのチームとして課題意識を持ち、本質に目を向けて行動していくことが必要だ。

『Akka実践バイブル』を読んだ(後半)

前半はこちらです

hiramekun.hatenablog.com

第9章 メッセージのルーティング

EIPのうち、ルーターパターンについて。メッセージのルーティングを行う理由がパフォーマンスやスケーリングである場合は、Akkaに組み込まれている最適化されたルーターを使用する。しかしながら、メッセージの内容が主要な関心ごとである場合は、通常のアクターを使って役割ごとにメッセージを振り分けるのが良いとされている。

Akkaのルーターを使った負荷分散

システムのパフォーマンスを向上させるために、異なるアクターに負荷を分散させる。 Akkaには組み込みのルーターが2種類存在している。

プールルーター

以下のようにしてプールルーターは設定ファイルを用いて生成できる。

val router = system.actorOf(
  FromConfig.props(Props(new Rootie(actorRef))),
  "poolRouter"
)
akka.actor.deployment {
  /poolRouter {
    router = balancing-pool
    nr-of-instances = 5
 }
}

リモートにルーティーを作成したい場合は、方法は複数あるが、簡単な方法としてFromConfigRemoteRouterConfigに変えるだけで良い。

また、動的にルーティーのサイズを変更したい場合も、設定ファイルにリサイザー機能のオプションをカスタマイズすることで細かくリサイズの条件を指定できる。

ルーターはルーティーを生成しているため、ルーティに対するスーパバイザーでもある。デフォルトのルーターを使用する場合、ルーティーは常にスーパバイザーに障害をエスカレーションする。ルーターがさらに障害をエスカレーションすると、障害の起こったルーティーのみではなく、ルーター自体が再起動されてしまい、全てのルーティーが再起動されてしまう。そこで、ルーターの生成寺に独自の戦略を与えることで障害の発生したルーティーだけが再起動されるように変更できる。

グループルーター

ルーティーを自分でインスタンス化し、明示的にルーターの管理下に置く。これにより、ルーティーを生成するタイミングの制御が可能になる。
グループの場合は、ルーティーパスのリストを指定することでルーティーを生成する。具体的には、CreatorアクターがRootieアクターをRootie1, Rootie2の二つ生成し、その二つのアクターをルーティーに指定する場合は以下のように設定する。

akka.actor.deployment {
  /groupRouter {
    router = round-robin-group
    routees.paths = [
      "/user/Creator/Rootie1",
      "/user/Creator/Rootie2"
    ]
  }
}

ルーティーが終了してもグループルーターは引き続きルーティーにメッセージを送信するため、ルーティーを管理しているアクターが、子アクターの終了に対してハンドリングする必要がある。

その他

よく出てくるbecomeメソッドについて定義を確認した。定義を見ればわかるように、Receiveを渡すことで、メッセージを受け取った時の挙動を変えることができる。なので、状態を持ったアクターのなかによく現れることがある。

  /**
   * Changes the Actor's behavior to become the new 'Receive' (PartialFunction[Any, Unit]) handler.
   * Replaces the current behavior on the top of the behavior stack.
   */
  def become(behavior: Actor.Receive): Unit = become(behavior, discardOld = true)

第10章 メッセージチャネル

  • point to point channel:今まで扱ってきたアクターは全てこれ。
  • publish/subscribe channel:送信者がメッセージを必要としている受信者を知ることなく、複数の受信者にメッセージを送信する。

publish/subscribe channel

チャネルは、全てのsubscriberがメッセージを受け取れるようにする。受信者が自身でsubscribeするため、動的に受信者の数を変えることができ柔軟に変更できる。

満たされるべき要件は以下の通り。

  • 送信側は、メッセージをpublishできる必要がある。
  • 受信側は、チャネルのsubscribeとunsbscribeができる必要がある。

Akkaのイベントストリーム

Akkaでは EventStreamを使用することでpublish/subscribeできるようになる。全てのアクターシステムには一つの EventStream があり、どのアクターからでも利用できる。アクターは特定のメッセージ型をsubscribeでき、誰かがこれをpublishすると受け取ることができる

subscribeは、送受信するアクターが自分で設定する必要はない。必要なものは、次の2点のみで、これさえあれば任意の場所でsubscribeさせることができる。

  • subscribeするアクターの参照
  • subscribeの設定を行うEventStreamへの参照
system.eventStream.subscribe(
  actorRef,
  classOf[Message]
)

publishは以下のようにして行われる

system.eventStream.publish(msg)

ローカルシステム全体からメッセージを送信し、それを収集するための解決策として、EventStreamは活用できる。例えばActorLoggingは内部的にEventStreamを用いてシステム全体からログを収集している。以下は ActorLoggingapply関数。

def apply[T: LogSource](system: ActorSystem, logSource: T): LoggingAdapter = {
  val (str, clazz) = LogSource(logSource, system)
  new BusLogging(system.eventStream, str, clazz, system.asInstanceOf[ExtendedActorSystem].logFilter)
}

カスタムイベントバス

「条件xを満たす時のみメッセージを送る」という場合を考えてみる。EventStreamではメッセージの型を元に送信するか否かを決定するため、フィルタリングができない。受信先でフィルタリングはできるが、それ以外の方法も考えてみる。独自のpublish/subscribe channel を作成することでこれを実現することができる。

EventBusというインターフェースを用いて作成することができる。EventBusってそもそも何かというと、お互いを知る必要がなく双方向に通知が行えるような仕組みのことのようだ。

RRiBbit – What is an Eventbus

An Eventbus is a mechanism that allows different components to communicate with each other without knowing about each other. A component can send an Event to the Eventbus without knowing who will pick it up or how many others will pick it up. Components can also listen to Events on an Eventbus, without knowing who sent the Events. That way, components can communicate without depending on each other. Also, it is very easy to substitute a component. As long as the new component understands the Events that are being sent and received, the other components will never know.

EventBus には3つのエンティティを指定する。

  • Event: busにpublishされるイベント全てを表す型。
  • Subscriber: イベントバスに登録するsubscriberの型。Akkaの場合はActorRefであり、ActorEventBus をミックスインすることで登録することが多い。
  • Classifier: イベントを送信するときのSubscriberの選択に使用する分類子を定義。

Classificationトレイトをミックスインすることで、classifyメソッドにより、EventからClassifierを抽出する。

  /**
   * Returns the Classifier associated with the given Event
   */
  protected def classify(event: Event): Classifier

つまり、

eventBus.subscribe(actorRef, classifier) // classifierの登録
eventBus.publish(msg) // ここで、msgが上でsubscribeしたclassifierであるならば、subscriberが選択され、実際に送信される。

特殊チャネル

デッドレターチャネル

処理または配信できない全てのメッセージを含むチャネル。(デッドメッセージキューとも呼ばれる。)   このチャネルを監視すると、処理されていないメッセージがわかる。
Akka内部では、DeadLetterListenerpreStartでDeadLetterを受け取るEventStreamをsubscribeする形で実装されている。

  override def preStart(): Unit = eventStream.subscribe(self, classOf[DeadLetter])

保証配信チャネル

リモートアクターを使用する場合、間のネットワークが死んだりしているとメッセージが消失してしまう。ReliableProxyを使うと、この問題が解決し、リモートアクターに対して送信できる可能性が高まる。
今コードを見たら @deprecated("Use AtLeastOnceDelivery instead", "2.5.0") と書いてあったので、非推奨になってるっぽい。

第11章 有限状態マシンとエージェント

状態を扱うための方法として、新しく二つの方法を紹介する。

有限状態マシン(Finite Stage Machine: FSM)

Akkaドキュメントより引用

FSMは、次の式の関係の集合として記述できます。
State(S) x Event(E) -> Actions (A), State(S')
これらの関係は、次のように解釈されます。
状態SでイベントEが発生した場合は、アクションAを実行して状態S'に遷移する必要があります

エージェントを使った状態共有の実装

現在エージェントは非推奨になっており、Akka Typed を使うことが推奨されている。

2.6.0-RC1でstableになったAkka Typedを試してみる - Candy, Vitamin or Painkiller

speakerdeck.com

第12章 ストリーミング

データのストリームとは、終わりがない要素の配列のことで、以下の時に存在している。

  • プロデューサーがストリームに要素を提供
  • コンシューマーがストリームから要素を読み込む

akka-streamは有限のバッファで無条件のストリームを処理する方法を提供する。また、akka-httpでは内部でakka-streamを使用している。

基本的なストリーム処理

以下の3つの存在を考える。

  • 要素のプロデューサー
  • 処理ノード
  • 要素のコンシューマー

sourceとsinkを使う

akka-streamを使うには、以下のステップが必要になる。

  • 処理フローの定義:ストリーム処理コンポーネントのグラフを定義。
  • 処理フローの実行:アクターシステムでの実行。グラフからアクターに変換される。

ファイルをコピーする簡単な例を考える。要素の供給源である Source と要素の吸収源である Sink をつなげることによって、
sourceからsinkにデータを直接送るGraphを定義する。

val source: Source[ByteString, Future[IOResult]] =
  FileIO.fromPath(inputFile)
                                                       
val sink: Sink[ByteString, Future[IOResult]] =
  FileIO.toPath(outputFile, Set(CREATE, WRITE, APPEND))
                                                       
val runnableGraph: RunnableGraph[Future[IOResult]] =
  source to sink

マテリアライズとは

runnableGraph.run() はimplicitな Materializer を必要とする。これは、RunnableGraph をアクターに変換してグラフを実行する。具体的には、

  • グラフに存在する入力と出力が全て接続されているか確認
  • SourceとSinkに紐づくアクターをそれぞれ生成。
  • そのアクターの間にpub/sub関係を結ぶ。

さらに、PublisherとSubscriberの間では、リクエストされた以上のメッセージを送信しない仕組みになっており、メモリがオーバーフローしないように送受信をしている。

SourceとSinkでは、グラフがまてリアライズされた時に補助値を提供する。ファイルの場合はFuture[IOResult]を提供する。どの結果をKeepするかを指定でき、それによってどのマテリアライズされた値を保持するかを指定できる。

フローによるイベントの処理

FlowはSourceとSinkの間に使えるコンポーネントで、全てのストリームロジックをキャプチャする。たくさんの処理を組み合わせてFlowとして構成される。

Flowの定義を見ておく。入力と出力が一つずつ定義されていることがわかる。第3の型指定はMaterializeなので、Flowの補助値(副作用のようなものと捉えている)が得られるものと考えられる。

/**
 * A `Flow` is a set of stream processing steps that has one open input and one open output.
 */
final class Flow[-In, +Out, +Mat]

例えば、ファイルからByteStringを受け取って、それをパースする処理を考える。すると、Flowの各コンポーネントは以下のようになる。

// ファイルから受け取ったByteStringをStringに変換するFlow
val frame: Flow[ByteString, String, NotUsed] =
  Framing.delimiter(ByteString("\n"), maxLine).map(_.decodeString("UTF8"))

// ByteStringをcase classであるEventにマッピングするFlow
val parse: Flow[String, Event, NotUsed] =
  Flow[String].map(LogStreamProcessor.parseLineEx)
    .collect { case Some(e) => e }
    .withAttributes(ActorAttributes.supervisionStrategy(decider))

// 合成されたフロー
val composedFlow: Flow[ByteString, Event, NotUsed] = flow via parse

// runnableグラフの構築
val runnableGraph: RunnableGraph[Future[IOResult]] =
    source.via(composedFlow).toMat(sink)(Keep.right)

ストリームのエラー処理

上記のようなFlowの場合にエラーが発生した時、Futureは失敗した値を返す。そこで、パースが失敗した時にその例外を無視して他の行のパースは行わせる事を考える。ストリームにもスーパバイザー戦略を定義できる。

val decider: Supervision.Decider = {
  case _: LogParseException => Supervision.Resume
  case _ => Supervision.Stop
}
                                                                 
val parse: Flow[String, Event, NotUsed] =
  Flow[String].map(LogStreamProcessor.parseLineEx)
    .collect { case Some(e) => e }
    .withAttributes(ActorAttributes.supervisionStrategy(decider))

BidiFlowとは

2つの入力と2つの出力を持つグラフコンポーネント。多分Bidirectionの略。ソースコードに添付してある図を見るとわかりやすい。

  /**
   * Wraps two Flows to create a ''BidiFlow''. The materialized value of the resulting BidiFlow is determined
   * by the combiner function passed in the second argument list.
   *
   * {{{
   *     +----------------------------+
   *     | Resulting BidiFlow         |
   *     |                            |
   *     |  +----------------------+  |
   * I1 ~~> |        Flow1         | ~~> O1
   *     |  +----------------------+  |
   *     |                            |
   *     |  +----------------------+  |
   * O2 <~~ |        Flow2         | <~~ I2
   *     |  +----------------------+  |
   *     +----------------------------+
   * }}}
   *
   */

つまり、I1が全体の入力。O2が全体の出力とする。そして、上記のO1とI2をインターフェースとして持っているFlowをつなげる事で、任意のフロート接続ができるようにするというもの。具体的には以下のように使う。

val bidiFlow = BidiFlow.fromFlows(inFlow, outFlow)
val flow = bidiFlow.join(filterFlow)

ストリーミングHTTP

実際にakka-httpを使ってPOSTとGETをどのように処理するかを学んだ。詳細は本のソースコードなどに譲る。

マーシャリングとアンマーシャリング

HTTPレスポンスを返す際に、アプリケーションで使われているオブジェクトからHTTPレスポンス用のデータフォーマットに変換する事をマーシャリングという。その逆をアンマーシャリングという。

グラフDSL

入力と出力を任意の数だけ持たせられるグラフDSL。例えば、Eventを受け取って以下の5つのイベントをそれぞれ別々のFlowで処理する場合はこのようなコードになる。

  • Jsonに変換し、ByteStringを返す
  • okのログ出力
  • warningのログ出力
  • errorのログ出力
  • criticalのログ出力
type FlowLike = Graph[FlowShape[Event, ByteString], NotUsed]
                                                                 
def processStates(logId: String): FlowLike = {
  val jsFlow = LogJson.jsonOutFlow
  Flow.fromGraph(
    GraphDSL.create() { implicit builder =>
      import GraphDSL.Implicits._
      // all logs, ok, warning, error, critical, so 5 outputs
      val bcast = builder.add(Broadcast[Event](5))
      val js = builder.add(jsFlow)
                                                                 
      val ok = Flow[Event].filter(_.state == Ok)
      val warning = Flow[Event].filter(_.state == Warning)
      val error = Flow[Event].filter(_.state == Error)
      val critical = Flow[Event].filter(_.state == Critical)
                                                                 
      bcast ~> js.in
      bcast ~> ok ~> jsFlow ~> logFileSink(logId, Ok)
      bcast ~> warning ~> jsFlow ~> logFileSink(logId, Warning)
      bcast ~> error ~> jsFlow ~> logFileSink(logId, Error)
      bcast ~> critical ~> jsFlow ~> logFileSink(logId, Critical)
                                                                 
      FlowShape(bcast.in, js.out)
    })
}

これまでで考えると、返り値が Flow[Event, ByteString, NotUsed]でないのが違和感を感じるかもしれないが、FlowはGraphを継承している。つまり入力と出力を一つずつ持つ特別な場合のShapeがFlowなのである。

コンシューマーとプロデューサーの仲介

需要と供給のバランスをどうやって取るかについて。バッファーを用いてこれを解決する。

デフォルトではバックプレッシャーが有効になっており、ストリームを処理できる。

Akka Streamsで実装するリアクティブストリーム | Think IT(シンクイット)より引用

バックプレッシャープロトコルには、下流のサブスクライバーが受信してバッファリングできる要素の数が定義されています。バックプレッシャーは、サブスクライバーが処理できる以上の要素を、パブリッシャーがパブリッシュしないことを保証します。

さらには、bufferやexpandのようなメソッドを使って変更を加えることもできる。

第13章 システム統合

  • Alpakkaを使用して、外部システムと連携する。
  • akka-httpを使用してHTTPプロトコルをサポートする。

メッセージエンドポイント

正規化パターン

  • 様々な種類のメッセージを共通の標準化されたメッセージに変換する。
  • システムは、メッセージが様々な外部システムから送られてくる事を気にせずメッセージを処理できる。
  • ルーターを介して、トランスレーターに送り、共通のメッセージに正規化して送るパターンもある。

標準データモデルパターン

システム間の接続要件が増加すると、エンドポイントが多くなり、正規化パターンでは複雑になってしまう。   標準データモデルパターンでは、全てのシステムで共通のインターフェースを実装して、共通メッセージを利用するエンドポイントを持たせる。

エンドポイントが共通形式のメッセージを受信し、そのあと独自のメッセージに変換する。逆にエンドポイントが独自のメッセージを共通形式のメッセージに変換して、共通インターフェースを介して送信する。

標準データモデルパターンが、アプリケーションの個々のデータ形式と外部システムで利用されるデータ形式に間接参照を提供する一方で、正規化パターンは1つのアプリケーションに閉じているという点です。この間接参照がもたらす利点は、新しいアプリケーションをシステムに追加する時に共通メッセージを処理するトランスレーターだけ用意すればよいということです。既存システムの変更は必要ありません。

Alpakkaを用いたエンドポイントの実装

Alpakkaで提供されているコンポーネントを使うと、簡単に外部システムと接続できる。 例として、ディレクトリ内のファイル変更検知や、AMQPを用いたメッセージ送信があげられる。今回は共通形式のメッセージを Order クラスに指定している。それぞれイベントをSourceに変換しているので、toMatRunnableGraphに変換する事でストリーム処理が可能になる。

外部システムからメッセージの受信

Alpakkaを用いてファイルの変更を検知するSourceを生成する。

object FileXmlOrderSource {
    def watch(dirPath: Path): Source[Order, NotUsed] =
      DirectoryChangesSource(dirPath, pollInterval = 500.millis, maxBufferSize = 1000)
        .collect {
          case (path, DirectoryChange.Creation) => path
        }
        .map(_.toFile)
        .filter(file => file.isFile && file.canRead)
        .map(scala.io.Source.fromFile(_).mkString)
        .via(parseOrderXmlFlow)
  }

// stream処理できる
val consumer: RunnableGraph[Future[Order]] =
  FileXmlOrderSource.watch(dir.toPath)
    .toMat(Sink.head[Order])(Keep.right)

AMQPの場合

object AmqpXmlOrderSource {
  def apply(amqpSourceSettings: AmqpSourceSettings): Source[Order, NotUsed] =
    AmqpSource.atMostOnceSource(amqpSourceSettings, bufferSize = 10)
      .map(_.bytes.utf8String)
      .via(parseOrderXmlFlow)
}

// stream処理できる
val consumer: RunnableGraph[Future[Order]] =
  AmqpXmlOrderSource(amqpSourceSettings)
    .toMat(Sink.head)(Keep.right)

AMQP自体なんぞやという感じだったのでこの辺の記事を読んだ。

www.slideshare.net

Advanced Message Queuing Protocol - Wikipedia

外部システムへのメッセージ送信

外部システムへのメッセージ送信も受信時と同様に、今度は AmqpSink を用いれば共通メッセージを介したSinkが生成できる。

HTTP

今回はakka-httpを用いてRESTインターフェースを実装する。

~を使う事で、ルートの定義やディレクティブを組み合わせることができる。以下の例は getOrderpostOrders のいずれかとマッチすると読むことがができる。

val routes = getOrder ~ postOrders

~の定義は以下の通り。

/**
 * Returns a Route that chains two Routes. If the first Route rejects the request the second route is given a
 * chance to act upon the request.
 */
def ~(other: Route): Route = ...

具体的にディレクティブがどのようにリクエストを処理するかを追ってみる。以下はgetOrderの詳細。

 // getはMethodDirectivesの一つ。GETリクエスト以外をrejectする。
def getOrder = get {

  // pathPrefixはPathDirectivesの一つ。PathMatcherを受け取り、
  // スラッシュ以降のパターンマッチしていない部分に適用する。
  pathPrefix("orders" / IntNumber) { id => 
    // IntNumberはNumberMatcher, PathMatcherをextendしている。リクエストのpathから数値を取り出す。

    // onSuccessはFutureDirectivesの一つ。Futureの値を取り出して、inner scopeのrouteを実行する。
    onSuccess(processOrders.ask(OrderId(id))) {
      case result: TrackingOrder =>
        // completeはRouteDirectivesの一つ。引数からリクエストを完了する。
        complete(
          <statusResponse>
            <id>
              {result.id}
            </id>
            <status>
              {result.status}
            </status>
          </statusResponse>
        )
                                               
      case result: NoSuchOrder =>
        complete(StatusCodes.NotFound)
    }
  }
}

第14章 クラスタリング

6章では、決まった数のノードを利用して分散アプリケーションを構築した。詳細はこちら

『Akka実践バイブル』を読んだ(前半) - ひらめの日常

さらに、クラスターを使うと、分散アプリケーションで使用するノード数を動的に増減させることができる。

なぜクラスタリングを用いるか

クラスターは動的なノードのグループ。各ノードはアクターシステムを持っている。クラスターに所属するメンバーノードのリストは現在のクラスターの状態として維持される。アクターシステムはお互いにこの情報を伝達し合う。具体的には次のような機能を持っている。

クラスターメンバーシップ

クラスターはシードノード、マスターノード、ワーカーノードで構成される。

  • シードノード:クラスターを起動するために必要。クラスターの起点であり、他のノードとの最初の接点として機能する。
  • マスターノード:ジョブの制御と監督。
  • ワーカーノード:マスターに仕事を要求し、処理結果をマスターに返す。

シードノードが起動したのちは、全てのノードを独立に依存関係なく起動することができる。

これとは別に、リーダー という責務がある。これは、メンバーノードの状態が Up なのか Down なのかを判断する。そして、実際にクラスターにノードを参加させたり、離脱させたりする。クラスターに存在するどのノードもリーダーになる可能性がある。

障害が起きた時

障害が起きた時 Unreachable という状態になる。クラスターは到達不能なノードを検出する。到達不能なノードがある限り、リーダーはアクションを実行することができないので、まずは到達不能ノードを Down させる必要がある。

クラスターないのノードで起きた障害を通知されたい場合は、 subscribe する事でイベント通知をアクターで受けることができる。

クラスタリングされたジョブの処理

マスターは最初にワーカーを作成してから、メッセージをブロードキャストする必要がある。それはルーターで実現するが、クラスターとルーターを合わせるためには既に存在する有効なプールを ClusterRouterPoolへ渡すことが必要になる。 具体的な流れは次の通り

  • マスターが ClusterRouterPool, BroadcastPool を用いてルーターとなり、ワーカーをルーティーとして生成する。
  • マスター(=ルーター)では、ワーカー(=ルーティー)に対して、jobを始めるというメッセージをboradcastする
  • マスターは、ワーカーからタスク開始したいというリクエストを受信したら、タスクに必要なメッセージを送信する。(ここはルーターの機能は用いず、個々に対して送信する)

ワーカーはマスターに対して Enlistメッセージを送信することで、ジョブへの参加を表明する。マスターではEnlistメッセージで受け取った ActorRef を用いて監視したり、ジョブ終了時に全てのワーカーを停止したりできる。

第15章 アクターの永続化

akka-persistenceモジュールを使って、アクターの状態を永続化する方法について。akka-persistenceモジュールを使うと、クラスターのノードに障害が起きたり置き換えられたとしても継続して動作するようなアプリケーションを構築できる。

クラスター拡張の方法は二つある。

イベントソーシング

イベントソーシングとは

成功した全ての操作をイベントとしてジャーナルに保存する。そのイベント列から指示された操作を実行することで値を得る。

アクターのイベントソーシング

イベントソーシングを用いることによる大きなメリットは、データベースへの書き込みとデータベースへの読み込みを明確に分離できること。アクターを回復するときのみ、ジャーナルから読み取りが発生する。

永続アクター

永続アクターは1)イベントから状態を回復するか、2)コマンドを処理するか、の2つのもノードのうちどちらかで動作する。

  • イベント:アクターが処理を正しく実行したという証跡を残すためのもの。
  • コマンド:アクターに処理を実行させるために送信するメッセージ。

永続アクターではまず以下のことが特徴となる

  • PersistentActor トレイトを利用する
  • receiveメソッドを定義する代わりに、 receiveCommandreceiveRecover の2つのメソッドを定義する必要がある。receiveRecoverでは、アクターの回復中に過去のイベントとスナップショットを受け取るために使う。

persist メソッドでコマンドをイベントとして永続化する。二番目に渡されている引数は、永続化されたイベントを処理する関数であり、今回のupdateState はアクターの計算結果を更新する。

val receiveCommand: Receive = {
    case Add(value) => persist(Added(value))(updateState)
    case Subtract(value) => persist(Subtracted(value))(updateState)
    case Divide(value) => if (value != 0) persist(Divided(value))(updateState)
    case Multiply(value) => persist(Multiplied(value))(updateState)
    case PrintResult => println(s"the result is: ${state.result}")
    case GetResult => sender() ! state.result
    case Clear => persist(Reset)(updateState)
  }

永続化されたイベントを処理する関数は非同期に呼ばれるが、akka-persistenceではこの関数の処理が完了する前に次のコマンドが処理されないようにする。このためにメッセージをいくらか蓄えるために、パフォーマンス上のオーバーヘッドが多少ある。

receiveRecoverでは、コマンドが正しく処理された時と全く同じ処理を実行する必要がある。回復しようとしているアクターと同じ persistenceIdでジャーナルに保存されたイベントは以前と同じ結果を得るために処理される。アクターが起動したり再起動した際に receiveRecoverメソッドが使われる

val receiveRecover: Receive = {
  case event: Event => updateState(event)
  case RecoveryCompleted => log.info("Calculator recovery completed")
}

スナップショット

アクターが回復するまでの時間を短くするにはスナップショットが利用できる。スナップショットは別の SnapshotStore に保存される。アクターの回復時には、最新のスナップショットが渡され、その後にそのスナップショットが保存された時点から発生したイベントが渡される。最新のスナップショット以前のイベントは渡されてこない。

永続クエリー

永続アクターの回復処理以外でジャーナルを検索するためのモジュール。最適なユースケースは、永続アクターから連続的にイベントを読み取り、クエリーに適した形で別のデータベースに保存すること。基本的には2種類のクエリがある。具体的に eventsByPersistenceIdcurrentEventsByPersistenceId の違いを見てみる。

  • eventsByPersistenceId: PersistenceActor が受け取ったイベントをその順番に取得する。このストリームは終了することはなく、新しくアクターがイベントを受け取った時にそのイベントをpushする。
  • currentEventsByPersistenceId: 基本的には eventsByPersistenceId と同じ挙動が、現在の状態まで到達した時点で、このストリームは終了する。

シリアライズ

独自のシリアライザーを設定するには、設定ファイルから指定する。

akka {
  actor {
    serializers {
      basket = "aia.persistence.BasketEventSerializer" // 独自のシリアライザーを登録
      basketSnapshot = "aia.persistence.BasketSnapshotSerializer"
    }
    serialization-bindings {
      "aia.persistence.Basket$Event" = basket // シリアライズが必要なクラスにシリアライザーをバインド
      "aia.persistence.Basket$Snapshot" = basketSnapshot
    }
  }
}

クラスターシングルトンとクラスターシャーディング

概念

クラスターシングルトン: アクターのインスタンスがAkkaクラスター内の同じロールを持つノード上でただ一つだけ存在することを保証した上でそのアクターを実行できる。もしこのクラスターシングルトンが停止した場合、他のノード上で開始する。

シングルトンやシャーディングについて仕組みが理解しづらかったのでこちらの記事を読んだ。非常にわかりやすい記事だった。

Akka Clusterで超レジリエンスを手に入れる | Think IT(シンクイット)

Akka Clusterで超レジリエンスを手に入れる(その2) | Think IT(シンクイット)

クラスターの中にノードがいくつか存在していて、その中でさらにシャードが分かれている。(下記図は Akka Clusterで超レジリエンスを手に入れる(その2)より引用) f:id:thescript1210:20210121191342p:plain

ShardRegion がメッセージを受け取り、それが ShardCoordinator にシャードの位置を聞く。ShardCoordinatorからの回答を用いて、メッセージは目的のシャード・エンティティに届けられる。

ShardCoordinatorクラスタ内で1つのみ存在すべきであり、Akkaのクラスタシングルトンで実装されている。さらに ShardCoordinator は情報を復旧できる必要があるので、 Akka Distributed Data を使用して、データを復元することができるようになっている。

Akka実践バイブルにおけるシャーディング

  • ShardShoppersClusterSharding を起動し、ShardRegion への参照を取得する。
  • ShardRegionはメッセージを受け取った時、シャードである ShardedShopper にメッセージを転送する。
  • ShardedShopper は自身の一意となるentityIdの指定方法と、どのシャードに配置されるかのshardIdの指定方法を明示する。これは ClusterSharding がスタートする時に渡され、どこに何を配置するのかを把握するのに必要となる。
class ShardedShoppers extends Actor {

  ClusterSharding(context.system).start(
    ShardedShopper.shardName,
    ShardedShopper.props,
    ClusterShardingSettings(context.system),
    ShardedShopper.extractEntityId,  // アクターを一意に定めるid
    ShardedShopper.extractShardId // どのシャードに位置するかを定めるid
  )

  def shardedShopper = {
    ClusterSharding(context.system).shardRegion(ShardedShopper.shardName) // shardRegionへの参照を取得
  }

  def receive = {
    case cmd: Shopper.Command =>
      shardedShopper forward cmd 
  }
}