NGINXでのMySQL:非ブロッキングサーバーでのブロッキングライブラリの使用

ご存じのように、負荷の高いサーバーを開発する場合、ソケットを操作するためのイベントモデルがよく使用されます。 システムの主要コンポーネントはepollです(FreeBSDとWindowsには独自のソリューションがありますが、Linuxに焦点を当てましょう)。 唯一のブロッキング呼び出しであるepoll_wait関数は、関心のあるすべてのネットワークイベントに関する情報を返します。 同様に、もちろん、有名なNGINXサーバーも動作します。



イベント駆動型のプログラミングモデルは、コードをひっくり返すようにコードを非常に独特なものにします。 しかし、この問題はそれほどひどいものではありません。 別の問題があります-イベント指向のコードで、本来は意図されていなかった既存のライブラリの使用です。 そのようなライブラリがブロック呼び出し(たとえば、connect、recvなど)を行うと、イベントモデル全体がその意味を失う可能性があります。 他のすべてのクライアントは、そのような呼び出しが1つ終了するまで待機します。これは、深刻な製品を作成している場合は完全に受け入れられません。



元々ノンブロッキング環境での使用を目的としていなかったライブラリの1つは、libmysqlclientクライアントライブラリです。 ただし、NGINXではしばしば必要になります。 drizzleやHandlerSocketなど、 NGINXからMySQLにアクセスできるソリューションがいくつかあります(標準のNGINXアップストリームメカニズムを使用して簡単なプロトコル実装するのは非常に簡単です)。 ただし、それにもかかわらず、最も便利なのは、標準のlibmysqlclientライブラリとSQL言語の全機能を使用することです。



コンテキストの切り替えとインターセプト



ブロッキングコールの問題には簡単な解決策があります。 ブロッキングコードを非ブロッキングコードに変換するには、ブロッキングコールをインターセプトし、それを非ブロッキングコールに置き換えれば十分です。ブロッキングが必要な場合は、メインサーバーループに進みますが、予想されるイベントが発生した場合は、同じ場所に戻ります左。 つまり このようなユーザー固有のスレッドを作成します。 かなり安くなります。 マルチタスクの混雑は彼にとって役に立たないので、適切なタイミングですべてのコンテキスト切り替えを行います。



最初に、実行のスレッドをブロックできる呼び出しを見つけます。

主なものは次のとおりです。



このリストの最後の関数-poll-はあまり正直に見えません それ自体が非ブロッキング動作の兆候である場合があります。 ただし、libmysqlclientはそれを使用するため、インターセプトする必要があります。 明らかに、epoll_waitもブロックしていますが、ブロックコードがそれを使用しないことを願っています。 選択呼び出しはまだありますが、多くの問題があるため、(神に感謝!)ますます使用されなくなりました。 また除外されます。



これらの関数はlibcで定義されているため、コードが動的にリンクしている場合は、標準のインターセプト手法を使用するあらゆる機会があります。 読み取りの例を示します。



typedef ssize_t (*read_pt)(int fd, void *buf, size_t count); static read_pt orig_read; ssize_t read(int fd, void *buf, size_t count) { ssize_t ret; for(;;) { /*   read */ ret = orig_read(fd, buf, count); if (!mtask_scheduled || ret != -1 || errno != EAGAIN) return ret; /*   ;    */ if (mtask_yield(fd, NGX_READ_EVENT)) { errno = EIO; return -1; } } } ... /* -   */ orig_read = (read_pt)dlsym(RTLD_NEXT, "read");
      
      





ここで、 mtask_yield



は、コンテキストをメインイベントループに切り替える関数です。 通常のブロックコードがブロックされる必要があるときに呼び出されます。 mtask_scheduled



コンテキストを切り替えてブロック動作をシミュレートするか、標準的な方法で動作させるかを決定できるマクロ。 明らかに、ハンドラの外側では、すべてのコンテキストスイッチが干渉するだけです。 さらに、NGINX自体によって行われた呼び出し(たとえば、リクエストの送受信)は、明らかに私たちの助けを必要とせず、もともとノンブロッキング動作のために設計されました。



また、 read



操作が実行されるソケットは非ブロックモードにする必要がありaccept



。そのconnect



は、フックされたconnect



およびaccept



fcntl



に対応する呼び出しを行う必要がありaccept







コンテキスト



ユーザー固有の実行コンテキストとは何ですか? これはスタック+レジスタです(シグナルを受信するためのマスクもありますが、シグナルからジャンプしないため、今は興味がありません)。 すべてが非常に単純な場合、1つのプロセスのフレームワーク内でコンテキストを切り替えることができるのは明らかです。 これには標準ツールがあります。





NGINXに固定します





NGINXでは、生成されるコンテンツはビューハンドラーによって生成されます。



static ngx_int_t ngx_http_mtask_handler(ngx_http_request_t *r);







このハンドラーは、NGX_HTTP_CONTENT_PHASEフェーズのハンドラーのリストに追加されます。



 h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); *h = ngx_http_mtask_handler;
      
      







通常の使用では、ハンドラーはクライアントに返されるバッファーを含むチェーン(ngx_chain_t)を作成し、その後関数を呼び出します



ボディは明らかにソケットに完全に「収まらない」場合がありますが、もちろんブロックは発生せず、NGINX自体はクライアントハンドラーの完了後に送信に関与します。



そのため、ハンドラーでブロッキング操作を実行できるようにします。 これを行うには、次を実行します。



 /*  ,   */ getcontext(&ctx->wctx); ctx->wctx.uc_stack.ss_sp = ngx_palloc(r->pool, mlcf->stack_size); ctx->wctx.uc_stack.ss_size = mlcf->stack_size; ctx->wctx.uc_stack.ss_flags = 0; ctx->wctx.uc_link = NULL; makecontext(&ctx->wctx, &mtask_proc, 0); /*     */ mtask_wake(r, MTASK_WAKE_NOFINALIZE); /*  NGINX',        ,   ,      ;    */ r->main->count++;
      
      







mtask_wake関数は、次の基本的なことを行います。



 /*      */ /*        - */ mtask_setcurrent( r ); /*    */ /*       ! */ swapcontext(&ctx->rctx, &ctx->wctx); /*   ,  ,    ! */ if (!mtask_scheduled) { /*   */ if (!(flags & MTASK_WAKE_NOFINALIZE)) ngx_http_finalize_request(r, NGX_OK); return 1; } /*   ,   -   */ /*    -    */ /*         */ mtask_resetcurrent();
      
      







mtask_yield関数は最も重要な作業を行います-ブロッキング呼び出しをNGINXイベントに変換し、制御をメインスレッドに返します。



 /*    NGINX        */ c = ngx_get_connection(fd, mtask_current->connection->log); c->data = mtask_current; /*   /  NGINX */ if (event == NGX_READ_EVENT) e = c->read; else e = c->write; e->data = c; e->handler = &mtask_event_handler; e->log = mtask_current->connection->log; ngx_add_event(e, event, 0); /*         ,     */ swapcontext(&ctx->wctx, &ctx->rctx); /*  !    /.  */ ngx_del_event(e, event, 0); ngx_free_connection( c );
      
      







NGINXイベントハンドラーは1つの基本的なことを行います。I/ Oイベントが発生するとコンテキストを切り替えます。



 static void mtask_event_handler(ngx_event_t *ev) { ... mtask_wake(r, wf); ... }
      
      







また、ユーザー固有のスレッドでは、元々ノンブロッキング動作用に設計されたNGINX関数を呼び出すことができないことにも言及する価値があります。 ただし、このような関数は、ngx_http_send_headerおよびngx_http_output_filterから呼び出される可能性が最も高くなります。 これらの呼び出しを防ぐために、次のように現在の接続をバッ​​ファリングモードにします。



 c->write->delayed = 1
      
      







スレッドの最後で、このフラグはリセットされ、データがクライアントに送信されます。 明らかに、このソリューションは大量のデータを出力するのには適していませんが、ほとんどの場合、このタスクはそれだけの価値はありません(そして、それでもまだ少し美しい方法で解決できます)。



libmysqlclientを固定します





ブロッキングコードを実行するメカニズムにより、MySQLへのアクセスは簡単です。 まず、最も一般的なCONTENT_PHASEハンドラーが作成されます。 ユーザー空間のスレッドのプロトタイプ関数は、通常のハンドラーのプロトタイプと完全に一致することを思い出してください。 したがって、コードのブロック性を忘れて、標準フォームハンドラーでlibmysqlclientライブラリの標準ツールを使用します。



 ngx_int_t ngx_http_mysql_handler(ngx_http_request_t *r) { ... mysql_real_connect(...) ... mysql_query(...) ... mysql_store_result(...) ... mysql_fetch_row(...) ... }
      
      





データはフィールドごとにプレーンテキストで表示され、ngx_chain_tチェーン内のフィールドごとに1つのリンクを使用します。 これにより、NGINX自体の内部でクエリ結果を利用する簡単な機会が得られます。 これを行うには、mysql_subrequestディレクティブを実装します。これは、別のロケールで記述されたMySQLクエリを実行し、このコマンドに引数として渡される変数に結果を割り当てます(以下の例を参照)。 その後、変数を使用して、たとえば、データベースから取得した目的のバックエンドへの接続をプロキシしたり、データベースにアクセスできないスクリプトに値を転送したりできます。



ハンドラー自体は、通常どおり(CONTENT_PHASEフェーズでは-ここでは「正直な」非ブロッキングコードが必要です)ではなく、mtaskモジュールの構成で登録されます。



 ngx_http_mtask_loc_conf_t *mlcf; mlcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_mtask_module); mlcf->handler = &ngx_http_mysql_handler;
      
      











このモジュールの機能を示すnginx.conf構成の例。



 server { ... #      server # unix socket access (default) mysql_host localhost; #mysql_user theuser; #mysql_password thepass; #mysql_port theport; mysql_database test; mysql_connections 32; #          # .. NGINX       #       . mtask_stack 65536; #  ! location /select { mysql_query "SELECT name FROM users WHERE id='$arg_id'"; } location /insert { mysql_query "INSERT INTO users(name) VALUES('$arg_name')"; } location /update { mysql_query "UPDATE users SET name='$arg_name' WHERE id=$arg_id"; } location /delete { mysql_query "DELETE FROM users WHERE id=$arg_id"; } #     location /pass { #  name       $name mysql_subrequest /select?id=$arg_id $name; #   $name   proxy_pass http://myserver.com/path?name=$name; } ... }
      
      







アドレスでモジュールを表示およびダウンロードできます



github.com/arut/nginx-mtask-module

github.com/arut/nginx-mysql-module



みんなありがとう!



All Articles