JavaScript - TECH PLAY - TECH PLAY

TECH PLAY

JavaScript

イベント

マガジン

技術ブログ

こんにちは、サイオステクノロジー武井です。今回はasync/awaitの動きをOSカーネルのレイヤーから追うことで理解を深めようとしてみます。 1. async/awaitはむずい async / await 、なかなかにとっつきにくい概念だと思います。 「非同期処理には   async   を付ける」「 await   しないと結果が取れない」——ルールとしては覚えられます。コードも動きます。でも、こんな疑問にぶつかった経験はないでしょうか。 await   している間、プログラムは「止まって」いるの? 止まっていないの? 「待つ」のに、なんで速くなるの? await   を付け忘れると Promise が返ってくるのはなぜ? そもそも誰が「処理が終わった」ことに気づいて、続きを実行してくれるの? これらイベントループやepollの仕組みを知っていると、スッキリ解決するのではと思い、今回、一筆したためました。 キーになるのは、 イベントループ という仕組みと、さらにその下で動く   OS カーネルの epoll   という機構です。 async / await   はこの2つの上にかぶせられた「一番上の薄い皮」にすぎません。皮だけを眺めて中身を推測しようとするから難しいのであって、下の層から順に積み上げれば、実はかなり素直な仕組みです。 この記事では、その水面下を「イベントループと fd」→「実際のコードの動き」の順で見ていきます。 2. イベントループとは何者か 「待つ」のはもったいない プログラムがデータベースや外部 API にアクセスすると、返事が来るまでの待ち時間が発生します。CPU の感覚では、この待ち時間は途方もなく長い。メモリアクセスを「1秒」とすると、ネットワークの応答待ちは「数週間〜数ヶ月」に相当します。 つまり I/O 待ちの間、CPU は膨大にヒマをしています。「このヒマな時間に別の仕事をさせたい」——これが非同期処理のすべての動機です。 1本のスレッドで、全部の「待ち」を見張る 古典的なやり方は、リクエストごとにスレッドを用意して、それぞれに待たせる方式でした。しかしスレッドは1本あたり数MBのメモリを食うため、同時接続数が多くなると厳しいことになります。 そこで発想を逆転させます。 1本のスレッドが、大量の「待ち」をまとめて見張り、準備ができたものから順に処理する 。レストランでいえば、客1組ごとにウェイターを張り付かせるのではなく、1人のウェイターが全テーブルを担当し、「呼ばれたテーブルにだけ行く」方式です。 この「ぐるぐる回りながら、準備できた仕事を順に捌く」ループこそが イベントループ です。 ここで2つの重要な部品が登場します。 epoll   と   fd   です。 fd (ファイルディスクリプタ)とは? fd(ファイルディスクリプタ)   は、OS が通信の窓口(ソケットなど)1つ1つに割り振る、ただの整数です。DB への接続なら fd9、外部 API への接続なら fd10、というように、「どの待ち先か」を番号で識別できます。 ポイントは、 fd は単なる番号であって、中身のデータでも処理でもない こと。「9番の窓口」「10番の窓口」という札だと思ってください。 epoll とは? epoll   は Linux カーネルの機能で、やることは一言でいうと: 大量の fd を登録しておくと、「データが届いた/送れる状態になった」fd だけをまとめて教えてくれる イベントループの   epoll.wait()   は、カーネルへの「どれか準備できるまで寝てるから、できたら起こして」という依頼です。1万個の fd を登録していても、実際に用があるのが2個なら、2個分の番号だけが返ってきます。全部を1個ずつ見回る必要がない——ここが決定的に効率的です。 fd とコールバックは「別の場所」に保管される もう1つ、仕組みの肝になるのが情報の置き場所です。 カーネル が持つのは「監視すべき fd の一覧」と「準備できた fd の一覧」——つまり 番号だけ 「fd9 の準備ができたら、どの処理(コールバック)を実行するか」という**対応表(登録簿)**は、Node.js でいえば   libuv (ランタイムの C エンジン)が持つ カーネルからの通知は「fd9 が準備完了」という番号だけ。libuv はその番号を鍵にして、自分の登録簿から「fd9 → このコールバック」を引き当て、実行キューに積みます。番号で照合して、初めて「続きの処理」が動き出すわけです。 この分業を頭に入れた上で、実際のコードを見てみましょう。 3. コードで追う:DB アクセスと API アクセスが「すれ違いで」進む 例えば、こんなコードがあるとします ユーザー情報を   DB   から、最新のお知らせを   外部 API   から取ってくる——お互いに関係のない2つの非同期処理です。 // DBからユーザーを取得する処理 async function getUser ( ) { console . log ( "A-1: DBに問い合わせを実行" ) ; const user = await db . findUser ( 9 ) ; // ← DBアクセス(待ち発生) console . log ( "A-2: DB応答が届いた!" ) ; return user ; } // 外部APIからお知らせを取得する処理 async function getNews ( ) { console . log ( "B-1: APIに問い合わせを実行" ) ; const news = await api . fetchNews ( ) ; // ← APIアクセス(待ち発生) console . log ( "B-2: API応答が届いた!" ) ; return news ; } // 2つを同時にスタートさせて、両方の完了を待つ const [ user , news ] = await Promise . all ( [ getUser ( ) , getNews ( ) ] ) ; console . log ( "C: 両方そろった!" ) ; 実行すると、ログはこの順で出ます。 A-1: DBに問い合わせを実行 B-1: APIに問い合わせを実行 ← DBの返事を待たずにBが始まっている! A-2: DB応答が届いた! B-2: API応答が届いた! C: 両方そろった! 注目してほしいのは   A-1 の直後に B-1 が出る ことです。DB の応答(A-2)を待たずに、API への問い合わせ(B-1)が始まっています。 await   で「待っている」はずなのに、なぜ次の処理へ進めるのか——ここで、さきほどのイベントループと fd が登場します。 図と対応させて、裏側を追う await db.findUser(9)   の裏側で起きていることを、4枚の図で「 登録 → すれ違い → 完了 」の順に追っていきます。登場人物は、コードを走らせる アプリケーション 、番号だけを見張る カーネル 、対応表を持つ libuv 、そしてその対応表である libuv の登録簿 の4者です。 ステップ1: getUser   の番—— await   に到達し、fd9 を登録する getUser()   が呼ばれ、A-1 のログを出し、 await db.findUser(9)   に到達します。ここで 登録フェーズ が動きます。   ①   アプリケーションが libuv に「 DB アクセス処理 を頼む」と依頼します。DB への問い合わせも、この依頼にのって送り出されます。 ②   libuv がカーネルに「この接続を見張って」と依頼します。 ③   カーネルの監視対象に、DB 接続の窓口である   fd9   が登録されます。以後カーネルは fd9 を見張り続けます。 ④   libuv が自分の登録簿に「 fd9 → コールバック処理(A-2 以降の続き) 」を控えます。 そして決定的なのが次の一歩です。 getUser   は DB の返事を 待ちません 。①〜④の依頼と登録だけ済ませて、その場で中断し、 制御をイベントループに返します 。ウェイターが注文を厨房に通して、すぐテーブルを離れるのと同じです。 ステップ2:空いた隙間で、 getNews   の番——fd10 も登録する getUser   が席を離れた瞬間、イベントループは次の仕事に取りかかれます。それが   getNews()   です。B-1 のログが出て、 await api.fetchNews()   に到達し—— まったく同じ①〜④が、今度は別の fd(fd10)で 繰り返されます。   ①   アプリケーションが libuv に「 API アクセス処理 」を依頼(ここで API への問い合わせも実行)。 ② ③   libuv がカーネルに依頼し、カーネルの監視対象に   fd10   が加わります。 ④   libuv の登録簿に「 fd10 → コールバック処理(B-2 以降の続き) 」が控えられます。 そして   getNews   も席を離れます。 これが「 A-1 の直後に B-1 が出る 」カラクリです。 DB の待ち時間という「ヒマ」に、API への問い合わせという別の仕事が差し込まれた わけです。ステップ2の状態を見てください。カーネルは fd9(DB)と fd10(API)の2つを並べて見張っており、libuv の登録簿にも2つの続きが控えられています。スレッドはどちらの返事も来ていないので   epoll.wait()   で眠っており、CPU 消費はゼロです。 ステップ3:返事が届いた——fd9 の完了フェーズ やがて DB の応答が届きます。ここからは 完了フェーズ です。   ①   見張っていたカーネルが「fd9 にデータが届いた」ことを検知し、libuv に 完了通知 を送ります。流れるのは「 fd9 が準備完了 」という番号だけです。 ②   libuv がその番号を鍵に登録簿を引き、「fd9 → コールバック処理」を 取得 します。 ③   そのコールバックがアプリケーション上で 実行 され、 getUser   が   await   の次の行(A-2)から再開 します。 ステップ4:少し遅れて、fd10 の完了フェーズ 少し遅れて API の応答も届けば、今度は fd10 について まったく同じ①〜③ が走ります。   ①   カーネルが「fd10 が準備完了」を libuv に通知。 ②   libuv が登録簿から「fd10 → コールバック処理」を取得。 ③   コールバックが実行され、 getNews   が   B-2 から再開 します。 両方そろったところで   Promise.all   が解決し、C のログが出て完了です。 ところで、ここまで   await db.findUser(9)   と一息に書いてきましたが、この一行が裏で返している   Promise   という存在に、まだきちんと触れていませんでした。実はこの Promise こそ、いま追った図の世界と、私たちが書く JavaScript のコードとをつなぐ「窓口」です。同じ図をもう一度、今度は Promise の視点から見直してみましょう。 4. Promise とは何か db.findUser(9)   はその場で「券」を返す await db.findUser(9)   という一行は、実は2つの動作に分かれています。 const promise = db . findUser ( 9 ) ; // ① findUser がすぐ何かを返す const user = await promise ; // ② その「何か」の中身を取り出す db.findUser(9)   を呼ぶと、 その場ですぐ戻り値が返ってきます 。とはいえ、返ってくるのは DB のデータ……ではありません。まだ返事は届いていないのですから当然です。代わりに返るのが   Promise ——「結果は今はまだ無いけれど、あとで必ず渡す(か、失敗を伝える)と約束する券」です。レストランの 番号札 を思い浮かべてください。料理(データ)と引き換えるための、 非同期処理の引換券 です。 これを図のステップ1に重ねると、こうなります。 db.findUser(9)   を呼んだ瞬間——①で DB への問い合わせが実行され、③で fd9 がカーネルの監視対象に登録された、あの瞬間——アプリケーションの手元に残るのは引換券(Promise)だけで、中身はまだ空っぽです。   券には「ステータス」がある——監視対象と連動している この引換券は、ただの紙きれではありません。処理が今どこまで進んだかという ステータス を持っていて、その移り変わりを1枚にすると次の図になります。   図の見方はこうです。 左の   pending (保留中)   が出発点です。 db.findUser(9)   を呼んで券を受け取った直後—— ステップ1 の状態にあたります。まだ結果は無く、券は「保留中」のまま宙に浮いています。 ここから券は 2方向のどちらか一方 にしか進みません。返事が無事に届けば、右上の   fulfilled (成功)   へ。上向きの矢印に添えた   resolve (成功が確定)   がその引き金です。これは ステップ3 で受信コールバックが走り、データが揃った瞬間に相当します。 逆に通信エラーなどで失敗すれば、右下の   rejected (失敗)   へ。下向きの矢印の   reject (失敗が確定)   が引き金です。 そして矢印の向きに注目してください。 pending   から出ていく矢印はあっても、戻ってくる矢印はありません 。一度   fulfilled   か   rejected   に確定した券は、二度と別の状態に変わらない——これが「引換券のステータスは 一方通行 」という意味です。だから同じ Promise を何度   await   しても、確定済みの同じ結果が返ってきます。 さらに肝心なのは、この券のステータスが、 前章の図でカーネルが見張っている監視対象(fd9)の状態と連動している ことです。fd9 にまだ返事が来ていないステップ1〜2の間、券は   pending 。ステップ3でカーネルが「fd9 準備完了」を通知し、受信コールバックがデータを揃えた瞬間に、券は   pending → fulfilled   へ動きます。つまり Promise とは、 イベントループが見張っている対象のステータスを、開発者が JavaScript の世界から覗くための窓口 でもあるわけです。カーネル・libuv という水面下の状態が、この一枚の券に映し出されている、と考えると腑に落ちます。 券から中身を取り出す:本来は   .then() 券を握っているだけでは、データは使えません。「券が   fulfilled   になったら、この処理をして」とあらかじめ登録しておく必要があります。それが   .then()   です。 const promise = db . findUser ( 9 ) ; // 引換券を受け取る(pending) promise . then ( ( user ) => { // 「券が確定したら、これを実行して」と予約 console . log ( user . name ) ; } ) ; これ、見覚えがないでしょうか。前章の完了フェーズ(ステップ3)で「fd9 → 続きの処理」が登録簿に控えられ、券が確定した瞬間に呼び出される——構造はまったく同じです。 .then()   に渡した関数こそ、図の登録簿に入る「続き」の正体のひとつです。 await   は「券が中身に化けるのを待つ」ための、読みやすい書き方 .then()   でもデータは取り出せますが、処理が連なると入れ子が深くなり読みづらくなります(いわゆるコールバック地獄)。そこで   await   の出番です。 // .then スタイル:券を受け取って、続きをコールバックで書く db . findUser ( 9 ) . then ( ( user ) => { console . log ( user . name ) ; } ) ; // await スタイル:券が中身に化けるのを待って、同期的に受け取る const user = await db . findUser ( 9 ) ; // ← 券の中身をそのまま変数へ console . log ( user . name ) ; await   は「右側の Promise(券)が   fulfilled   になるまで席を離れて待ち、確定したら中の値を取り出して返す」係です。 .then()   のコールバックを書く代わりに、 あたかも同期処理のように、上から下へ 書けるようになります。裏で起きていること(登録して席を譲る → 券が確定 → 続きを再開)は前章の図とまったく同じで、 await   はそれを読みやすい見た目に整えているだけ——ここでも「一番上の薄い皮」なのです。 「 await   を付け忘れると Promise が返る」理由 ここまで分かると、冒頭の疑問——「 await   を付け忘れると Promise が返ってくるのはなぜ?」——にも答えられます。 const x = db . findUser ( 9 ) ; // awaitなし console . log ( x ) ; // → Promise { <pending> }(券そのもの) const user = await db . findUser ( 9 ) ; // awaitあり console . log ( user ) ; // → { id: 9, name: "..." }(中身) await   を付けなければ、券(Promise)を中身に引き換える動作が走らないまま次の行へ進みます。だから手元には引換券が残ったまま。「付け忘れると Promise が返る」の正体はこれです。 async   は「この関数は引換券を返す」という宣言 ここで、ずっと脇役だった   async   の正体もはっきりさせておきましょう。 async   は関数に付ける 印 で、意味はとてもシンプルです。 async   を付けた関数の戻り値は、必ず Promise(引換券)に包まれる。 return user   と書いても、呼び出し側が受け取るのは   user   そのものではなく   Promise<user> 。途中で例外を投げれば、その券は   rejected   になります。「この関数はすぐには結果を返さない。まず引換券を渡して、中身はあとで確定させる」——それを宣言するのが   async   です。 async function getUser ( ) { return { id : 9 , name : "Alice" } ; // ← ただのオブジェクトを返しているつもりでも } const r = getUser ( ) ; console . log ( r ) ; // → Promise { <fulfilled> }(券に包まれている) そしてもう一つ大事なルールがあります。 await (席を離れて待つ動作)が使えるのは、 async   を付けた関数の中だけ です。裏返すと、関数の中で一度でも   await   したいなら、その関数は   async   にせざるを得ない。 await   する=「自分もすぐには終われず、結果はあとで返す」ということなので、当然その関数も引換券を返す関数になる、という筋の通った話です。 async   は呼び出し元へ「伝染」していく この2つのルールを組み合わせると、面白い——そして最初は戸惑う——現象が起きます。 async / await   が、呼び出しチェーンを上へ上へと伝播していく のです。 // 末端:DBを待つので await が要る → async が必須 async function getUser ( ) { const user = await db . findUser ( 9 ) ; return user ; // 戻り値は Promise<user> } // 中間:getUser() の中身を使いたい → await する → 自分も async に async function buildProfile ( ) { const user = await getUser ( ) ; // ここで await return ` ${ user . name } さん ` ; // これも Promise<string> になる } // 上位:buildProfile() の中身を使いたい → また await → また async async function handler ( req , res ) { const text = await buildProfile ( ) ; // ここでも await res . send ( text ) ; } 一番下の   getUser   が「DB を待つ」ために   async   になった瞬間、その戻り値は引換券になります。その中身を使いたい   buildProfile   は   await   するしかなく、すると   buildProfile   も引換券を返す関数( async )になる。さらにその上の   handler   も……と、 「あとで返る」という性質が、値を使うすべての呼び出し元へ玉突きで広がっていく わけです。 なぜ途中で断ち切れないのでしょう。「中間の関数が、こっそり結果を待って 同期的な値 にして返す」ことができれば伝播は止まります。しかしそれは、その場に立ち止まってスレッドをブロックすることに他なりません。イベントループの大前提—— 待つ間に他の仕事を捌く ——を壊してしまいます。だから「待ちがある」という事実は隠せず、型(Promise)として全経路に正直に現れる。これは面倒というより、 「この経路には待ちが含まれる」がコード上で追跡できる 、むしろ安全な設計だと考えると腑に落ちます。 では、この伝播はどこで止まるのか。答えは エントリポイント です。Express のリクエストハンドラ、 main() 、イベントリスナ——プログラムの一番外側で誰かが最後に受け止めれば、そこで鎖は終わります。普段のアプリ開発で末端からフレームワークまで   async   を意識せずに書けているのは、 この一番外側をフレームワークが引き受けてくれている からです。私たちは「待ちを使う関数に   async   を付ける」を末端でやるだけで、あとは自然に上流までつながります。 では、その登録簿に控えられる「続き」—— .then()   の中身にあたる 受信コールバック は、具体的に何なのでしょうか。次章では、それを自分の手で書いてみます。 5. 登録簿に入るのは「受信コールバック」 ここまでで「fd の準備ができたら、対応する処理が実行される」と説明してきました。では、その「対応する処理」——図の登録簿に入っているものの正体は、具体的に何でしょうか。 この受信コールバックは、普段は   fetch   や   axios 、DB ドライバといった ライブラリが内部で用意している という点です。「ライブラリが勝手にやってくれる」の正体は、ライブラリの中で誰かがこのコールバックを書いて登録している、というだけ。つまり、 自分でも書けます 。低レベルな姿から順に見ていきましょう。 レベル1:最も生の姿(ソケット + コールバック) タイマーもHTTPライブラリも使わず、「fd を監視して、読めたらコールバック」を手で書くと、こうなります。Node の   net   モジュールが、図の登録簿への登録を担当します。 const net = require ( "net" ) ; // APIサーバーにTCP接続する(ここでfdが1つ割り当てられる) const socket = net . connect ( 80 , "example.com" , ( ) => { // 接続できたらHTTPリクエストを"実行"する(図の①) socket . write ( "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" ) ; } ) ; // ★これが「登録簿に入るコールバック」そのもの★ // 「このソケット(fd)が読めるようになったら、これを実行して」と登録している socket . on ( "data" , ( chunk ) => { console . log ( "受信コールバックが起動:" , chunk . toString ( ) . slice ( 0 , 50 ) ) ; } ) ; socket . on ( "end" , ( ) => { console . log ( "受信完了" ) ; } ) ; socket.on("data", ...)   に渡している関数—— これが図の「fd9 → 受信コールバック」の中身 です。 .on("data")   を呼んだ瞬間、内部で「このfdが読める状態になったら、この関数を呼べ」と libuv の登録簿とカーネルの epoll に登録されます。ライブラリがやっていた「勝手に入れてくれる」の正体は、この   .on("data", callback)   の一行なのです。 ただしこれは コールバックスタイル なので、 await   はまだ使えません。 await   で受けたいなら、次のレベルで Promise に包みます。 レベル2:自分でPromiseを作って、awaitできるようにする 受信コールバックを   new Promise   の中に入れて、データが全部届いたら   resolve   する——これで   await   可能な関数を 自作 できます。 const net = require ( "net" ) ; // API取得を、await可能な関数として自作する function fetchFromApi ( ) { return new Promise ( ( resolve , reject ) => { const socket = net . connect ( 80 , "example.com" , ( ) => { socket . write ( "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" ) ; // ① 実行 } ) ; let received = "" ; // ★登録簿に入る受信コールバック(自分で書いている)★ socket . on ( "data" , ( chunk ) => { received += chunk . toString ( ) ; // 届いたぶんを溜める(届くたびに呼ばれる) } ) ; // 全部届いたらPromiseを解決する socket . on ( "end" , ( ) => { resolve ( received ) ; // ← ここで pending → fulfilled(ステップ3③の引き金) } ) ; // 失敗したらreject socket . on ( "error" , ( err ) => { reject ( err ) ; } ) ; } ) ; } // これで自作関数をawaitできる! async function main ( ) { console . log ( "問い合わせ前" ) ; const body = await fetchFromApi ( ) ; // ← 自分で作ったPromiseをawait console . log ( "受信完了:" , body . slice ( 0 , 50 ) ) ; } main ( ) ; このコードで、記事の第3章で見た図の全要素が、自分のコードとして揃います。 図の要素 上のコードの該当箇所 ステップ1① 問い合わせの実行 socket.write(...) 登録簿に入る受信コールバック socket.on("data", ...) ステップ3③の引き金(Promise解決) resolve(received) await   の続き console.log("受信完了", ...) new Promise   の中身が、これまで「ライブラリが書いていた低レベル処理」だったもの です。それを自分の手で書いているのがこのコードで、 axios   や   fetch   の内部も、煎じ詰めれば(エラー処理・タイムアウト・再接続などを足しつつ)これと同じことをやっています。 レベル3:「登録簿に入るコールバック」だけを最小で ライブラリも DB も API も使わず、「fd を監視して、読めたらコールバック」の骨組みだけを抜き出すと、こうなります。標準入力(これも立派な fd=0 です)で見せると一番シンプルです。 // 標準入力(fd=0)も監視対象のひとつ process . stdin . on ( "data" , ( data ) => { // ★これが登録簿に入るコールバック★ // 「fd0が読めるようになったら(=キー入力があったら)これを実行」 console . log ( "入力コールバックが起動:" , data . toString ( ) . trim ( ) ) ; } ) ; console . log ( "入力待ち(でもプログラムは固まっていない)" ) ; これを実行すると、 .on("data", ...)   の関数がカーネルの epoll 経由で「fd0 が読めるようになったら呼ばれる」よう登録され、キーを打つたびに起動します。 待っている間もプログラムは固まらない ——これこそ、イベントループが背後で回っている何よりの証拠です。 まとめ:登録簿の中身は「返事が来たら動く係」 3つのレベルを通して見えてくるのは、登録簿に入るものの正体です。 入るのは「API アクセス処理(これから叩く)」ではなく、 受信コールバック(返事が来たら処理する係) その実体は   .on("data", ...)   に渡す関数で、「fd が読めたら実行して」と libuv/epoll に登録される await   可能にするには、その受信コールバックを   new Promise   で包み、データが揃ったら   resolve   する 普段使う   fetch ・ axios ・DBドライバは、この「受信コールバック +   new Promise 」を、実運用に耐えるよう堅牢に書き上げたもの 普段のアプリ開発で、この低レベルコードを自分で書くことはまずありません。それでも「ライブラリの   await   の裏では   new Promise   とソケットの受信コールバックが動いている」と知っていれば、タイムアウトやコネクション管理でトラブルが起きたとき、 どの層を見ればいいかを切り分けられる ようになります。ライブラリをブラックボックスにしないための、確かな足場になるはずです。 おわりに かなり色々説明をはしょったような気もしますが、おおまかな概念は伝えきれたようにお思います。これでasync/await怖くなくなれば幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 2人がこの投稿は役に立ったと言っています。 The post 世界一わかりみの深いasync/awaitによる非同期処理 first appeared on SIOS Tech Lab .
Elasticsearchのデータ移行やバックアップを、スナップショット機能を使わずに「もっと手軽に、インデックス単位でサクッと行いたい」と思ったことはありませんか? そんなエンジニアの強い味方になるのが、オープンソースのCLIツール  elasticsearch-dump  (通称:elasticdump)です。 今回は、このツールの基本的な使い方から、実務で役立つ一歩進んだテクニックまでを解説します。 Elasticsearch を運用していると、以下のようなシーンに直面することがよくあります。 開発環境(Develop)の特定のインデックスだけを、ステージング環境(Staging)にサクッとコピーしたい インデックスの「マッピング」や「アナライザー」の設定だけを抽出して使い回したい ローカルファイル(JSON/CSV)にデータを退避させたい これらをGUIや面倒なAPIリクエストなしに、 使い慣れたCLIコマンド一発で解決してくれる のが、今回紹介する elasticsearch-dump です。 目次 1. elasticsearch-dumpとは? 2. クイックスタート:インストール方法 2.1. Node.js環境の用意 2.2. npmによるインストール 3. 基本的な使い方とユースケース 3.1. ユースケース①:開発環境からデータをダンプ 3.1.1. マッピング(構造定義)をダンプ 3.1.2. ドキュメントデータ(実データ)をダンプ 3.2. ユースケース②:特定のデータだけを絞り込んでダンプする(searchBody) 4. 知っておくと便利な一歩進んだ応用テクニック 4.1. データの移行中にオンザフライで変換をかける(–transform) 5. ダンプしたデータのリストア 5.1. マッピングの作成 5.1.1. ローカルで my_index_mapping.json を開く 5.1.2. インデックス名( “my_index” )の階層を除外してコピーする 5.1.3. Kibana Dev Tools (Console) で実行する 5.2. データの登録 5.2.1. _bulk 用の ndjson への変換 5.2.2. Kibana Dev Tools (Console) で実行する 6. まとめ:どんな時に使うべきか? 7. 参考リンク 1. elasticsearch-dumpとは? elasticsearch-dump  は、Elasticsearchのインデックスデータをインポート/エクスポートするためのNode.js製ツールです。[^1] 1 最大の特徴は、「 --input (入力元)から、 --output (出力先)へデータをストリーミングして送る」という極めてシンプルな設計にあります。 入力元と出力先には、ESのURLだけでなく、ローカルのJSON/CSVファイルや、標準入出力(stdin/stdout)、さらにはAWS S3なども直接指定できます。 2. クイックスタート:インストール方法 今回検証した環境は以下の通りです。 OS : Windows 11 (PowerShell) Node.js : v24.18.0 jq : 1.8.2 2.1. Node.js環境の用意 Node.js 公式サイト  よりインストーラーをダウンロードし、インストールします。 ※ホスト環境にNode.jsをインストールしたくない場合は、公式のDockerイメージを利用する方法もありますが、ここでは説明を割愛します。 2.2. npmによるインストール npmを使ってグローバル(またはローカル)にインストールします。 # グローバルインストール npm install elasticdump -g 💡  Windowsでのインストール先について  Windows環境でグローバルインストール( -g )した場合、一般的には下記のパス配下に配置されます。[^2] 2 C:\Users\<ユーザー名>\AppData\Roaming\npm 3. 基本的な使い方とユースケース elasticdump は主に「Analyzer(アナライザー)」「Mapping(マッピング)」「Data(ドキュメントデータ)」の3つのフェーズ( --type )に分けて処理を行います。 (ここでは Analyzer フェーズは省略します。また、–type には settings や policy, alias, template なども指定可能です。詳細は、公式ページを参照してください。) 3.1. ユースケース①:開発環境からデータをダンプ データを JSON 形式でダンプします。通常は「mapping ➔ data」の順にダンプします。 3.1.1. マッピング(構造定義)をダンプ # PowerShellでの実行例(改行は「`」を使用) elasticdump ` --input=http://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_mapping.json ` --type=mapping ⚠️  HTTPS環境(自己署名証明書など)でTLSエラーが出る場合  ElasticsearchがHTTPSでリクエストを受け付けており、証明書エラーが発生する場合は、一時的にTLS検証を無視する環境変数を設定してからコマンドを実行してください。[^3] 3 # PowerShellでの実行例 $env:NODE_TLS_REJECT_UNAUTHORIZED="0" elasticdump ` --input=https://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_mapping.json ` --type=mapping 3.1.2. ドキュメントデータ(実データ)をダンプ elasticdump ` --input=http://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_data.json ` --type=data 💡  Note : 出力ファイルフォーマットは「行区切りのJSON(Line-delimited JSON / NDJSON)」です。ファイル全体が1つの巨大なJSON配列ではないため、ストリーム処理に適しており、メモリ消費を最小限に抑えられます。 3.2. ユースケース②:特定のデータだけを絞り込んでダンプする(searchBody) 「管理者のログだけを抽出したい」「特定の期間のデータだけをバックアップしたい」という場合は、 --searchBody  オプションにElasticsearchのクエリ(DSL)を指定できます。 elasticdump ` --input=http://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_filtered_data.json ` --type=data ` --searchBody='{\"query\":{\"term\":{\"username\":\"admin\"}}}' ※クエリが複雑な場合は、別ファイル(例: search_body.json )に切り出して、 @  接頭辞を用いて読み込ませることも可能です。 ./search_condition/search_body.json { "query": { "term": { "username": "admin" } } } elasticdump ` --input=http://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_filtered_data.json ` --type=data ` --searchBody=@./search_condition/search_body.json 4. 知っておくと便利な一歩進んだ応用テクニック 4.1. データの移行中にオンザフライで変換をかける(–transform) 移行のタイミングで「特定の個人情報フィールドをマスクする」「新しいフィールドを追加する」といった簡易的なETL(Extract/Transform/Load)処理が可能です。 JavaScriptでドキュメントの操作関数を定義しておき、それを呼び出します。 ./transforms/anonymize.js module.exports = function (doc, options) { if (doc._source.email) { // メールアドレスのドメイン部分だけ残してマスクする例 doc._source.email = "anonymized@" + doc._source.email.split('@')[1]; } }; # トランスフォーム用スクリプトを指定して実行 elasticdump ` --input=http://elasticuser:elasticpassword@develop.es.com:9200/my_index ` --output=my_index_transformed_data.json ` --type=data ` --transform=@./transforms/anonymize.js 5. ダンプしたデータのリストア リストア先の環境で  elasticdump  や  curl  を利用できる場合は、それらを利用してリストアするのが簡単ですが、それらを利用できない場合には、ダンプした JSON ファイルを加工して、Dev Tools の Console からデータを投入します。 既存データを壊さないように、必ず  mapping ➔ data  の順でリストアします。 5.1. マッピングの作成 5.1.1. ローカルで my_index_mapping.json を開く エクスポートされたマッピングファイルは、一般的に以下のように「インデックス名」をルートキーに持つ構造になっています。 { "my_index": { "mappings": { "properties": { "title": { "type": "text" }, "price": { "type": "float" } } } } } 5.1.2. インデックス名( “my_index” )の階層を除外してコピーする Kibana Console でインデックスを新規作成する際、宛先インデックス名は URL パス( PUT /<インデックス名> )で指定するため、JSON 内の  "my_index": { ... }  という外枠(ラッパー)は不要です。 内側の  { "mappings": { ... } }  のブロックだけをコピーします。 5.1.3. Kibana Dev Tools (Console) で実行する Kibana の Dev Tools Console を開き、以下のようにリクエストを記述して実行(再生マークのボタンをクリック)します。 PUT /my_index { "mappings": { "properties": { "title": { "type": "text" }, "price": { "type": "float" } } } } 5.2. データの登録 5.2.1. _bulk 用の ndjson への変換 jq コマンドを jqのダウンロードサイト からダウンロードします。 jq コマンドをダウンロード後、my_index_data.json を _bulk  用の ndjson に加工します。 PowerShell と jq の文字コードの相性が悪いので、コマンドプロンプトを経由して jq を実行します。 (jqの実行ファイル名が jq-windows-amd64 の場合) cmd /c 'jqのインストールディレクトリ\jq-windows-amd64 -c "{\"index\":{\"_index\":\"my_index\",\"_id\":._id}}, ._source" my_index_data.json > bulk_formatted.ndjson' 5.2.2. Kibana Dev Tools (Console) で実行する Kibana Dev Toolsで以下のように  POST _bulk  を記述し、その後に続けて  bulk_formatted.ndjson  の内容を貼り付けて実行します。 POST _bulk { "index" : { "_index" : "my_index", "_id" : "1" } } { "title" : "...", "price" : ... } ... 6. まとめ:どんな時に使うべきか? Elasticsearch公式のスナップショット機能(Snapshot/Restore)は非常に強力ですが、S3などの共有リポジトリの登録が必要だったり、クラスタ全体の移行になりがちで、少々「重厚」です。 それに対して、  elasticsearch-dump  は以下のようなシチュエーションで抜群の機動力を発揮します。 開発環境と検証環境間で、数万〜数百万件程度の特定データをパパッと持ち運びたい スキーマ定義(マッピング)だけを手元でバージョン管理したい スナップショットの設定権限がない(AWS OpenSearch Serverless など制限のある環境を含む) 手元の開発環境に一つ入れておくだけで、データ運用の生産性が劇的に向上するおすすめのツールです。ぜひ皆さんのプロダクト運用や開発プロセスにも組み込んでみてください! 7. 参考リンク elasticsearch-dump GitHub公式リポジトリ 実行には事前に Node.js(npm)の実行環境が必要になります。 ↩︎ Windowsのユーザー環境によって、”AppData” フォルダは隠しフォルダになっている場合があるため、エクスプローラーの「表示」設定で「隠しファイル」にチェックを入れてアクセスしてください。 ↩︎ この設定はSSL/TLSの証明書検証を無効化するため、本番環境の公開ネットワーク等で実行する際はセキュリティリスクを考慮し、一時的な利用に留めてください。 ↩︎ The post 【保存版】elasticsearch-dumpで実現する、Elasticsearch の超柔軟なデータ移行・バックアップ手法 first appeared on Elastic Portal .
こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 以前 Vite+時代のVueプロジェクトのLinter/Formatter設定 2026年5月 という記事で、 vp create vue と pnpm create vue@latest のプロジェクト設定を比較しました。 そのとき分かったのは「Oxlint は .vue の <script> ブロックしか Lint できず、 <template> の Lint には引き続き ESLint(eslint-plugin-vue)が必要」ということでした。 その後、 Vize という Rust 製の Vue ツールチェーンを目にしました。 コンパイル・フォーマット・型チェック・Lint を1つの Rust 製コアでまかなうというプロジェクトで、公式サイトには eslint-plugin-vue との Lint ベンチマーク で 213 倍速いという良さげな数字も載っています。 私が気になったのは、やはり「テンプレートの Lint」です。 Vize は Vue テンプレートの Lint ができるのか、実際に手を動かして確かめました。 結論:できる 検証環境 やったこと 比較対象のプロジェクトを2つ用意する Vize 版の設定 検証用コンポーネント 検証結果 Vize ネイティブ CLI(vize lint) Oxlint ブリッジ(oxlint-vize) 比較:素の create-vue 側ではどうか 比較表 速度計測 結果 まとめ 結論:できる ここから先は、調査と整理を AI に手伝ってもらったログとなります🙇 Vize は Vue テンプレート( <template> ブロック)を Lint できる 方法は2つある ネイティブ CLI の vize lint Vize 自身のリンタ Oxlint プラグイン oxlint-plugin-vize Oxlint の中で Vize のルールを動かすブリッジ v-for の :key 漏れや v-html だけでなく、アクセシビリティ(a11y)ルールまで検出した ただし現状では Oxlint ブリッジ側はテンプレート診断の表示位置に制約がある(後述) 検証環境 Vize 0.228.0 oxlint-plugin-vize 0.228.0 @vizejs/vite-plugin 0.228.0 oxlint 1.70.0 vite 8.0.16 / vue 3.5.38 Node.js v24.16.0 / pnpm v11.7.0 やったこと 比較対象のプロジェクトを2つ用意する 比較のため、2 種類のプロジェクトを用意しました。 pnpm create vue@latest で作った素の Vue プロジェクトと、それを Vize ツールチェーンに載せ替えたプロジェクトです。 以降、それぞれ create-vue 版 / Vize 版と呼びます。 src/ の中身( HelloWorld.vue などのサンプルコンポーネント)は揃えてあります。 Vize の Getting Started を見ると、 create-vue のような scaffold コマンドは用意されていませんでした。 既存プロジェクトに対して、Vite プラグインと Lint 設定を手で足していくスタイルです。 Vize 版の設定 create-vue 版をコピーし、 @vitejs/plugin-vue を @vizejs/vite-plugin に、ESLint 一式を Vize の Lint に差し替えました。 vite.config.ts import { fileURLToPath, URL } from "node:url" import { defineConfig } from "vite" import vize from "@vizejs/vite-plugin" export default defineConfig( { plugins : [ vize() ] , resolve : { alias : { "@" : fileURLToPath( new URL ( "./src" , import.meta. url )), } , } , } ) コンパイラと Lint で共有される設定ファイル vize.config.ts は次のとおりです。 設定項目は Vite Plugin ガイド に一覧があります。 import { defineConfig } from "vize" export default defineConfig( { compiler : { sourceMap : true , } , vite : { scanPatterns : [ "src/**/*.vue" ] , } , } ) Lint は2経路を試したかったため、両方とも用意しました。 Oxlint ブリッジは .oxlintrc.json で設定します。 ネイティブ CLI の方は vize.config.ts の linter セクションで設定できます。今回の目的ではデフォルトで OK だったのでスキップです。 .oxlintrc.json { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue " ] , " jsPlugins ": [ " oxlint-plugin-vize " ] , " env ": { " browser ": true } , " settings ": { " vize ": { " preset ": " essential ", " helpLevel ": " short " } } , " categories ": { " correctness ": " error " } , " rules ": { " vize/vue/require-v-for-key ": " error ", " vize/vue/no-v-html ": " warn " } } package.json の scripts は、Lint 部分のみ抜粋すると次のとおりです。 { " scripts ": { " lint ": " oxlint-vize -c .oxlintrc.json -f stylish src ", " vize:lint ": " vize lint src --format stylish ", " vize:check ": " vize check src " } } oxlint-vize は oxlint-plugin-vize が提供する Oxlint のラッパーで、 vize バイナリはネイティブ CLI です。 検証用コンポーネント 両プロジェクトの src/components/ に、テンプレートへ違反を仕込んだ同じ Bad.vue を置きました。 < script setup lang = "ts" > const items = [ "a" , "b" , "c" ] const userHtml = "<b>hi</b>" </ script > < template > < ul > < li v-for= "item in items" > {{ item }} </ li > </ ul > < div v-html= "userHtml" ></ div > </ template > v-for に :key がなく、 v-html も使っています。どちらも <template> 内の問題です。 検証結果 Vize ネイティブ CLI(vize lint) まずは Vize 自身のリンタです。デフォルトの ecosystem プリセットで src/ を Lint しました。 $ vp exec vize lint src --format stylish src/components/Bad.vue 8:9 error vue/require-v-for-key Elements in iteration expect to have ' v-bind:key ' directives. Element: < li > 10:8 warning vue/no-v-html v-html can lead to XSS attacks. Avoid using it with user-provided content src/components/TheWelcome.vue 47:8 warning vue/no-unsafe-url Static URL attribute uses a potentially unsafe protocol or active data URL 47:8 warning a11y/anchor-is-valid < a > element has a javascript: href which is not recommended 1 error, 3 warnings in 10 files 仕込んだ vue/require-v-for-key (8:9)と vue/no-v-html (10:8)を、テンプレート内の正確な行・列付きで検出できました。 さらに、手を加えていないサンプルの TheWelcome.vue からも vue/no-unsafe-url と a11y/anchor-is-valid (アクセシビリティ)を拾っています。 eslint-plugin-vue の flat/essential には無いルールまでデフォルトで効いているようです。 Oxlint ブリッジ(oxlint-vize) 次に、Oxlint の中で Vize のルールを動かすブリッジです。 $ vp exec oxlint-vize -c .oxlintrc.json -f stylish src src/components/Bad.vue 2:2 error Elements in iteration expect to have ' v-bind:key ' directives. ( at < template > :8:9 ) vize ( vue/require-v-for-key ) 2:2 warning v-html can lead to XSS attacks. ( at < template > :10:8 ) vize ( vue/no-v-html ) ✖ 2 problems ( 1 error, 1 warning ) こちらでも同じ違反を vize(vue/...) として検出できました。 ただし注目したいのは表示位置です。 キャレットの行・列は 2:2 ( <script> ブロックの位置)を指していて、本来の <template> 内の位置は (at <template>:8:9) とメッセージ末尾にインライン表示されています。 これは Oxlint の JS プラグインの仕組み上の制約です。 JS プラグインは「抽出された Vue の script プログラム」内の範囲しか受け付けないため、テンプレートの位置はフォーマッタのアンカーに反映できません。 そこで Vize 側がサマリ文へ元 SFC の位置を差し込んで補っています。 この挙動は Vize の Oxlint ガイド にも記載があります。現状はネイティブ CLI のほうが素直な結果を得られます。 比較:素の create-vue 側ではどうか 同じ Bad.vue を create-vue 版に置いて、Oxlint と ESLint で Lint してみます。 まずは Oxlint(組み込みの vue プラグイン有効)です。 $ vp exec oxlint src/components/Bad.vue Found 0 warnings and 0 errors. 前回の記事のとおり、Oxlint 単体ではテンプレートの違反は 0 件でした。 <script> 内に違反が無いので何も出ません。 次に ESLint(eslint-plugin-vue の flat/essential )です。 $ vp exec eslint src/components/Bad.vue src/components/Bad.vue 1:1 error Component name " Bad " should always be multi-word vue/multi-word-component-names 8:5 error Elements in iteration expect to have ' v-bind:key ' directives vue/require-v-for-key ✖ 2 problems ( 2 errors, 0 warnings ) vue/require-v-for-key をテンプレートの位置(8:5)で検出できました。 これが前回「テンプレート Lint は ESLint 頼り」と書いた根拠です。 なお今回 vue/no-v-html が素通りしたのは、 flat/essential に含まれないからです。 flat/recommended 等にすれば eslint-plugin-vue でも検出できますが、Vize 合わせにしています。 比較表 項目 create-vue(Oxlint) create-vue(eslint-plugin-vue) Vize ネイティブ(vize lint) Vize Oxlint ブリッジ(oxlint-vize) テンプレート Lint 非対応( <script> のみ) 対応 対応 対応 require-v-for-key 検出せず 検出(8:5) 検出(8:9) 検出( at <template>:8:9 ) no-v-html 検出せず essential 外(recommended にあり) 検出(既定プリセット) 検出(warn 指定) a11y ルール なし 別途プラグイン要 デフォルトで検出 プリセット次第 表示位置の精度 — 正確 正確 キャレットは script、本来位置はサマリ 実装言語 Rust JS(vue-eslint-parser) Rust Rust(Oxlint 経由) ※ no-v-html と a11y の差は、有効にしたルールセット(プリセットや個別指定)の違いで生じます。 プリセットの一覧は Vize のルールページ にあります。 速度計測 oxlint + eslint(create-vue 既定のパイプライン) oxlint + vize plugin( oxlint-vize 。Oxlint の中で Vize のルールを動かす) vize native( vize lint ) 同じ内容の Vue SFC を 500 ファイル用意し、それぞれで Lint します。 # oxlint + eslint(create-vue 既定) $ vp exec oxlint src/bench && vp exec eslint src/bench # oxlint + vize plugin $ vp exec oxlint-vize -c .oxlintrc.json -f stylish src/bench # vize native $ vp exec vize lint src/bench 結果 3 構成とも同じ 500 ファイル( src/bench )だけを対象にした、ウォーム実行の wall-clock です。 構成 500 SFC の実行時間 テンプレート Lint oxlint 単体( <script> のみ・参考) 約 0.1 秒 しない eslint(eslint-plugin-vue・参考) 約 1.9 秒 する oxlint + eslint(create-vue 既定) 約 2.0 秒 する oxlint + vize plugin(oxlint-vize) 約 0.2 秒 する vize native 約 0.1 秒 する create-vue 既定の oxlint + eslint は、 eslint (eslint-plugin-vue)が約 1.9 秒を占めてほぼ支配します。 oxlint 自体は 0.1 秒と速いものの、 <script> しか見ません。 テンプレートも Lint するには eslint が必要で、そのぶん全体が重くなります。 一方、Vize を使う 2 つはどちらも 1 秒未満でした。 Oxlint エンジンに Vize のネイティブルールを足すぶん oxlint-vize は vize native よりやや重いものの、 eslint パイプラインとは桁が違います。 ざっくり、テンプレートまで Lint して vize native は oxlint + eslint のおよそ 20 倍、 oxlint + vize plugin でも約 10 倍速い、という結果でした。 まとめ Vize は Vue テンプレートの Lint ができる 手段は「ネイティブ vize lint 」と「 oxlint-plugin-vize (Oxlint ブリッジ)」の2つ 素直な結果と正確な位置が欲しいなら、現状はネイティブ CLI が良さそう 既存の Oxlint 運用に Vue の診断を混ぜたいならブリッジ。ただし表示位置はサマリのインライン表示で補う形 v-for の :key 漏れや v-html に加えて、a11y や no-unsafe-url までデフォルト設定で拾う 実際のプロジェクトで使うには、カスタム ESLint プラグインと同等のものを書ける API が欲しいなという感じでした。 公式ドキュメント でも Project-local JavaScript rule modules are not a stable Vize runtime API yet. とあり、自前ルールはまだ書けません。 独自ルールの移行は、Vize 側の対応待ち、もしくは自分で contribute、というところになりそうです。

動画

書籍