HighLoad Cup内で高負荷のシステムを開発した経験

Mail.Ruは、バックエンド開発者向けの興味深いチャンピオンシップ、HighLoad Cupを提供しています。 優れた賞品を獲得できるだけでなく、バ​​ックエンド開発者としてのスキルを高めることもできます。 環境の開発と設定の経験については、以下で説明します。



1.入力



旅行者サービスにWeb-APIを提供する高速サーバーを作成する必要があります。



サーバーの初期データには、ユーザー(旅行者)、場所(ランドマーク)、訪問(訪問)の3種類のエンティティがあります。 それぞれに独自のフィールドセットがあります。



次のクエリを実装する必要があります。



 GET / <entity> / <id>はエンティティデータを取得します
 GET / users / <id> / visitsでユーザーの訪問リストを取得します
 GET / locations / <id> / avgにより、関心のある平均地点を取得します
更新するPOST / <entity> / <id>
 POST / <entity> /作成する新規


要求ごとの最大ペナルティ時間はタンクタイムアウトに等しく、2秒(2kマイクロ秒)です。



ソリューションは1つのDockerコンテナにある必要があります。

検証に使用したアイロン:Intel Xeon x86_64 2 GHz 4コア、4 GB RAM、10 GB HDD。



そのため、タスクは基本的にシンプルですが、ドッカーの知識は0であり、50%程度の高負荷での開発経験です。

php7 + nginx + mysqlを記述するために、蓄積された経験をその後の作業で使用できるため、選択しました。



2. Docker



Dockerとは何かを理解しましょう。

Dockerは、オペレーティングシステムレベルの仮想化環境でのアプリケーションの展開と管理を自動化するためのソフトウェアです。 カーネルでcgroupsをサポートする任意のLinuxシステムに移植できるコンテナに、すべての環境と依存関係を含むアプリケーションを「パック」できます。また、コンテナ管理環境も提供します。
簡単に言えば、プロジェクトごとにnginx / php / apacheをローカルに設定する必要はなく、他のプロジェクトに追加の依存関係を取得する必要もありません。 たとえば、php7と互換性のないサイトがあります。このサイトで作業するには、apache2のphpモジュールを正しいバージョンに切り替える必要があります。 Dockerを使用すると、すべてがシンプルになります。プロジェクトでコンテナを起動して開発します。 別のプロジェクトに切り替え、現在のコンテナを停止して、新しいコンテナを作成します。



Dockerイデオロギー1プロセス-1コンテナー。 つまり、コンテナ内にphp、独自のmysqlを持つnginxです。 それらを組み合わせて構成するには、docker-composeを使用します。



docker-compose.ymlファイルの例
version: '2' services: mysql: image: mysql:5.7 #   environment: MYSQL_ROOT_PASSWORD: 12345 # root  volumes: - ./db:/var/lib/mysql #      ports: - 3306:3306 #   - _: nginx: build: context: ./ dockerfile: Dockerfile #    depends_on: [mysql] #  ports: - 80:80 volumes: - ./:/var/www/html #    ,     
      
      







以下を開始します。



 docker-compose -f docker-compose.yml up
      
      





すべてが機能し、接続があります。 検証のためにソリューションを送信し、タスクを注意深く読みます。すべてが1つのコンテナに収められている必要があります。 コンテナは、CMDまたはENTRYPOINTコマンドを介して実行されているプロセスが動作している間、順番に機能します。 複数のサービスがあるため、プロセスマネージャーを使用する必要があります-スーパーバイザー。



Dockerfileの構成
 FROM ubuntu:17.10 RUN apt-get update && apt-get -y upgrade \ && DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server mysql-client mysql-common \ && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql /var/run/mysqld \ && chown -R mysql:mysql /var/lib/mysql /var/run/mysqld \ && chmod 777 /var/run/mysqld \ && rm /etc/mysql/my.cnf \ && apt-get install -y curl supervisor nginx \ php7.1-fpm php7.1-json \ php7.1-mysql php7.1-opcache \ php7.1-zip ADD ./config/mysqld.cnf /etc/mysql/my.cnf COPY config/www.conf /etc/php/7.1/fpm/pool.d/www.conf COPY config/nginx.conf /etc/nginx/nginx.conf COPY config/nginx-vhost.conf /etc/nginx/conf.d/default.conf COPY config/opcache.ini /etc/php/7.1/mods-available/opcache.ini COPY config/supervisord.conf /etc/supervisord.conf COPY scripts/ /usr/local/bin/ COPY src /var/www/html #          # #RUN mkdir /tmp/data /tmp/db #COPY data_full.zip /tmp/data/data.zip ENV PHP_MODULE_OPCACHE on ENV PHP_DISPLAY_ERRORS on RUN chmod 755 /usr/local/bin/docker-entrypoint.sh /usr/local/bin/startup.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/startup.sh WORKDIR /var/www/html RUN service php7.1-fpm start EXPOSE 80 3306 CMD ["/usr/local/bin/docker-entrypoint.sh"]
      
      







CMDコマンド["/usr/local/bin/docker-entrypoint.sh"]は、コンテナーを起動してプロセスマネージャーを起動した後、環境の小さな構成を作成します。



プロセスマネージャーのセットアップ
 [unix_http_server] file=/var/run/supervisor.sock [supervisord] logfile=/tmp/supervisord.log logfile_maxbytes=50MB logfile_backups=10 loglevel=info pidfile=/tmp/supervisord.pid nodaemon=false minfds=1024 minprocs=200 user=root [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///var/run/supervisor.sock [program:php-fpm] command=/usr/sbin/php-fpm7.1 autostart=true autorestart=true priority=5 stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:nginx] command=/usr/sbin/nginx -g "daemon off;" autostart=true autorestart=true priority=10 stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:mysql] command=mysqld_safe autostart=true autorestart=true priority=1 stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:startup] command=/usr/local/bin/startup.sh startretries=0 priority=1100 stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0
      
      







priorityパラメータを使用すると、起動順序を変更できます。stdout_logfile/ stderr_logfileを使用すると、コンテナログにサービスログを表示できます。 最新のものはstartup.shスクリプトで、データベースにはアーカイブのデータが格納されています。

これで、最終的に最初のテストのためにあなたの頭脳を送ることができます。 dockerコマンドは、送信に使用するgitに似ています:



 docker tag < -> stor.highloadcup.ru/travels/< > docker push stor.highloadcup.ru/travels/< >
      
      





公式Webサイトhttps://cloud.docker.comに登録して、そこにコンテナを追加することもできます 。 そこで、githubまたはbitbucketでブランチを更新するときに自動アセンブリを構成し、他のプロジェクトで既製のイメージをベースとして使用できます。



3.サービス開発



高いパフォーマンスを確保するために、すべてのフレームワークを放棄し、裸のphp + pdoを使用することが決定されました。 フレームワークは、開発を大幅に促進しますが、スクリプトの実行時間を使用する多数の依存関係を引き出します。

出発点は、ルーティング要求と結果を返すindex.phpスクリプトです(ルーター+コントローラー)。 次のようなURLを使用します。



 /<entity>/<id>
      
      





正規表現を使用してルートとパラメーターを決定することを意味します。 これは非常に柔軟性があり、サービスを簡単に拡張できます。 しかし、ifオプションの方が高速であることが判明しました(エラーが発生する可能性はありますが、なぜですか?以下をお読みください)。



index.php
 $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $routes = explode('/', $uri); //    $entity = $routes[1] ?? 0; $id = $routes[2] ?? 0; $action = $routes[3] ?? 0; $className = __NAMESPACE__.'\\'.ucfirst($entity); if (!class_exists($className)) { //     header('HTTP/1.0 404 Not Found'); die(); } $db = new \PDO( 'mysql:dbname=travel;host=localhost;port=3306', 'root', null, [ \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'', \PDO::ATTR_PERSISTENT => true ] ); //   /** @var \Travel\AbstractEntity $class */ $class = new $className($db); // POST  (/ ) if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_SERVER['Content-Type'])) { //  json  $type = trim(explode(';', $_SERVER['Content-Type'])[0]); if ($type !== 'application/json') { header('HTTP/1.0 400 Bad Values'); die(); } } $inputJSON = file_get_contents('php://input'); $input = json_decode($inputJSON, true); // if ($input && $class->checkFields($input, $id !== 'new')) { $itemId = (int)$id; if ($itemId > 0 && $class->hasItem($itemId)) { $class->update($input, $itemId); header('Content-Type: application/json; charset=utf-8'); header('Content-Length: 2'); echo '{}'; die(); } //   if ($id === 'new') { $class->insert($input); header('Content-Type: application/json; charset=utf-8'); header('Content-Length: 2'); echo '{}'; die(); } //    -  header('HTTP/1.0 404 Not Found'); die(); } //    header('HTTP/1.0 400 Bad Values'); die(); } // GET  if ((int)$id > 0) { if (!$action) { //   ,    $res = $class->findById($id); if ($res) { $val = json_encode($class->hydrate($res)); header('Content-Type: application/json; charset=utf-8'); header('Content-Length: '.strlen($val)); echo $val; die(); } header('HTTP/1.0 404 Not Found'); die(); } //     $res = $class->hasItem($id); if (!$res) { header('HTTP/1.0 404 Not Found'); die(); } $filter = []; if (!empty($_GET)) { //  $filter = $class->getFilter($_GET); if (!$filter) { header('HTTP/1.0 400 Bad Values'); die(); } } header('Content-Type: application/json; charset=utf-8'); echo json_encode([$action => $class->{$action}($id, $filter)]); die(); } header('HTTP/1.0 404 Not Found'); die();
      
      







不器用に見えますが、すぐに動作します。 次は、AbstractEntityデータを処理するためのメインクラスです。 ここではすべてを簡単にします。挿入/更新/選択(すべてのソースコードはGiHubで表示できます)。 エンティティを持つクラスはすでにそこから形成されています。 たとえば、Usersエンティティを取り上げます。



フィルター

GETリクエストのデータの有効性をチェックし、データベース内のリクエストのフィルターを生成します。 以下のコードでは、注入などのチェック/フィルタリングは完全にありません。 戦闘プロジェクトこれを自宅で繰り返さないでください。



 public function getFilter(array $data) { $columns = [ 'fromDate' => 'visited_at > ', 'toDate' => 'visited_at < ', 'country' => 'country = ', 'toDistance' => 'distance < ', ]; $filter = []; foreach ($data as $key => $datum) { if (!isset($columns[$key])) { return false; } if (($key === 'fromDate' || $key === 'toDate' || $key === 'toDistance') && !is_numeric($datum)) { return false; } $filter[] = $columns[$key]."'".$datum."'"; } return $filter; }
      
      





ユーザーが訪れた場所を取得する

特定のユーザーの場所と評価が表示されます。上記で取得したフィルターも適用できます。



 public function visits(int $id, array $filter = []) { $sql = 'select mark, visited_at, place from visits LEFT JOIN locations ON locations.id = visits.location where user = '.$id; if (count($filter)) { $sql .= ' and '.implode(' and ', $filter); } $sql .= ' order by visited_at asc'; $rows = $this->_db->query($sql); if (!$rows) { return false; } $items = $rows->fetchAll(\PDO::FETCH_ASSOC); foreach ($items as &$item) { $item['mark'] = (int)$item['mark']; $item['visited_at'] = (int)$item['visited_at']; } return $items; }
      
      





年齢計算

これはおそらく、電報チャットで最も議論されたトピックでした。 ユーザーの生年月日は、タイムスタンプ形式(Linux時代の初めからの秒数)、たとえば12333444で設定されます。しかし、カウントダウンは1970年から続いており、まだ70年代以前に生まれた人がいます。 この場合、タイムスタンプは負の値になります(例:-123324)。 ユーザーは年齢でフィルタリングできます。たとえば、18歳以上の全員を選択できます。 データベースを照会するたびに年齢を計算しないようにするために、ユーザーをデータベースに追加する前に計算して、追加のフィールドに保存しました。



年齢計算機能:



 public static function getAge($y, $m, $d) { if ($m > date('m', TEST_TIMESTAMP) || ($m == date('m', TEST_TIMESTAMP) && $d > date('d', TEST_TIMESTAMP))) { return (date('Y', TEST_TIMESTAMP) - $y - 1); } return (date('Y', TEST_TIMESTAMP) - $y); }
      
      





データと応答は同時に生成され、時間の経過とともに変化しないため、テストに合格するにはTEST_TIMESTAMPを使用した「松葉杖」が必要です。 PHPの日付関数は、うるう年を指定すると、負のタイムスタンプを日付に完全に変換します。



データベース

データベースはエンティティに対して正確に作成され、すべてのフィールドサイズはTKでした。 DBエンジンInnoDb。 インデックスは、フィルターまたは並べ替えに関与するフィールドに追加されました。



Webサーバーとデータベースのセットアップ

パフォーマンスを向上させるために、インターネットで見つかった設定が使用されました。これらは、サービスを微調整するためにノブをひねる場所の始まりになっているはずです。



4.レポート処理、サービス設定の調整



PHPのソースコードは最小サイズであることが判明し、バックエンド開発者からシステム管理者に移行していることがすぐに明らかになりました。 迅速なテストは少量のデータで実行され、負荷がかかった状態でアプリケーションをテストするよりも、回答の正確性を検証するのに役立ちます。 本格的なテストは、12時間で2回しか実行できませんでした。 コンピューターでのテストは必ずしも明確な結果につながらなかった-すぐに動作し、テスト中にエラー502で落ちる可能性があるため、サーバーの応答を高速化するはずのmemcachedを構成できませんでした。



唯一良い点は、InnoDbの代わりにMyISAMエンジンを使用したことです。 テストでは、InnoDbの250秒ではなく133秒のペナルティ秒が与えられました。



ここで、nginx / mysql / php-fpm構成を適切に構成できなかったものについて- 1日のさまざまな時間における1つのソリューションの結果の重要なばらつき 。 同じ解決策の夕方/朝の結果にも広がりがあったので、これは私を完全に動揺させました。 「戦闘」チェックのインフラストラクチャがどのように配置されているのかわかりませんが、何かが車に干渉して負荷をかける可能性があることは明らかです(次の起動ソリューションを準備することは可能です)。 また、評価でアカウントがミリ秒単位になると、サーバーを微調整することができなくなります。



以下は、私が停止した構成です。



mysql
 [mysqld_safe] socket = /var/run/mysqld/mysqld.sock nice = 0 [mysqld] # # * Basic Settings # user = mysql pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp lc-messages-dir = /usr/share/mysql skip-external-locking # # Instead of skip-networking the default is now to listen only on # localhost which is more compatible and is not less secure. bind-address = 127.0.0.1 # # * Fine Tuning # key_buffer_size = 16M max_allowed_packet = 16M thread_stack = 192K thread_cache_size = 32 sort_buffer_size = 256K read_buffer_size = 128K read_rnd_buffer_size = 256K myisam_sort_buffer_size = 64M myisam_use_mmap = 1 myisam-recover-options = BACKUP table_open_cache = 64 # # * Query Cache Configuration # query_cache_limit = 10M query_cache_size = 64M query_cache_type = 1 join_buffer_size = 4M # # Error log - should be very few entries. # log_error = /var/log/mysql/error.log expire_logs_days = 10 max_binlog_size = 100M # # * InnoDB # innodb_buffer_pool_size = 2048M innodb_log_file_size = 256M innodb_log_buffer_size = 16M innodb_flush_log_at_trx_commit = 2 innodb_thread_concurrency = 8 innodb_read_io_threads = 64 innodb_write_io_threads = 64 innodb_io_capacity = 50000 innodb_flush_method = O_DIRECT transaction-isolation = READ-COMMITTED innodb_support_xa = 0 innodb_commit_concurrency = 8 innodb_old_blocks_time = 1000
      
      







nginx
 user www-data; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 2048; multi_accept on; use epoll; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; sendfile on; tcp_nodelay on; tcp_nopush on; access_log off; client_max_body_size 50M; client_body_buffer_size 1m; client_body_timeout 15; client_header_timeout 15; keepalive_timeout 2 2; send_timeout 15; open_file_cache max=2000 inactive=20s; open_file_cache_valid 60s; open_file_cache_min_uses 5; open_file_cache_errors off; gzip_static on; gzip on; gzip_vary on; gzip_min_length 1400; gzip_buffers 16 8k; gzip_comp_level 6; gzip_http_version 1.1; gzip_proxied any; gzip_disable "MSIE [1-6]\.(?!.*SV1)"; gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json image/svg+xml svg svgz; include /etc/nginx/conf.d/*.conf; }
      
      







nginx-vhost
 server { listen 80; server_name _; chunked_transfer_encoding off; root /var/www/html; index index.php index.html index.htm; error_log /var/log/nginx/error.log crit; location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { try_files $uri =404; include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/php/php7.1-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_read_timeout 3s; } }
      
      







php-fpmでは、具体的なことは何も達成されませんでした。



最終的にはデータ量が増加しましたが、残念ながら、ソリューションをさらに最適化するための十分な時間がありませんでした。 しかし、決勝戦の後、サンドボックスが開かれたので、決定を下し、結果をトップと比較することができます。



5.結論



このチャンピオンシップに参加できてうれしいです。 ドッカーの原理、つまり高負荷のためのサーバーのより深い構成を理解しました。 また、私は競争心と電報チャットが好きでした。 チャンピオンシップ全体では、C ++およびgoプログラマーがトップでした。 同様に、これらの言語のいずれかで書くこともできます。 しかし、私は自分が知っていること、そして一緒に仕事をしていることで自分の結果を見たかったのです。 これについてMail.Ruに感謝します。



6.参照



1. ソースコード

2. highloadcup.ruの最初のラウンドの競争



All Articles