usatie
  • NEW!
    NEW!  Connect Ideas Across Notes
    Save time and share insights. With Paragraph Citation, you can quote others’ work with source info built in. If someone cites your note, you’ll see a card showing where it’s used—bringing notes closer together.
    Got it
      • Create new note
      • Create a note from template
        • Sharing URL Link copied
        • /edit
        • View mode
          • Edit mode
          • View mode
          • Book mode
          • Slide mode
          Edit mode View mode Book mode Slide mode
        • Customize slides
        • Note Permission
        • Read
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Write
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invite by email
        Invitee

        This note has no invitees

      • Publish Note

        Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

        Your note will be visible on your profile and discoverable by anyone.
        Your note is now live.
        This note is visible on your profile and discoverable online.
        Everyone on the web can find and read all notes of this public team.

        Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Explore these features while you wait
        Complete general settings
        Bookmark and like published notes
        Write a few more notes
        Complete general settings
        Write a few more notes
        See published notes
        Unpublish note
        Please check the box to agree to the Community Guidelines.
        View profile
      • Commenting
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
        • Everyone
      • Suggest edit
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
      • Emoji Reply
      • Enable
      • Versions and GitHub Sync
      • Note settings
      • Note Insights New
      • Engagement control
      • Make a copy
      • Transfer ownership
      • Delete this note
      • Save as template
      • Insert from template
      • Import from
        • Dropbox
        • Google Drive
        • Gist
        • Clipboard
      • Export to
        • Dropbox
        • Google Drive
        • Gist
      • Download
        • Markdown
        • HTML
        • Raw HTML
    Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
    Create Create new note Create a note from template
    Menu
    Options
    Engagement control Make a copy Transfer ownership Delete this note
    Import from
    Dropbox Google Drive Gist Clipboard
    Export to
    Dropbox Google Drive Gist
    Download
    Markdown HTML Raw HTML
    Back
    Sharing URL Link copied
    /edit
    View mode
    • Edit mode
    • View mode
    • Book mode
    • Slide mode
    Edit mode View mode Book mode Slide mode
    Customize slides
    Note Permission
    Read
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Write
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # ISUCON13 チートシート ## 当日やること ``` ボトルネックを特定する。 ボトルネックに対して、変更は最小限で、どストレートにアプローチする。 ``` 1. ボトルネック特定のための素材を集める(最初の1h) 2. インデックスを貼るなどのお決まり(30min) 3. サービスをドッグフーディングしていじり倒す(30min) 4. アプリケーションマニュアルを読む(30min) 5. レギュレーション(特に採点マニュアル)を読む(30min) 6. main.goを読む(60min) --- (ここまでで2〜3時間。残りの5〜6時間で改善を行う) --- 7. ボトルネックの検討、アイディアの検討・計画(30 min) 8. 実装(15 min) 9. 7と8を繰り返す(できる限り7に時間をかける) ### ボトルネック特定のためのツール利用方法 - alpでnginxのアクセスログを解析する - pt-query-digestでmysqlのslow logを解析する - pprofでgoのcpu/heapのprofileを解析する - trdsqlでnginxのアクセスログを自由に解析する - dstatでcpu/memory/io/networkを解析する - iostatでioを解析する - topでcpu/memoryを解析する ## 最初の環境構築 ### sshの設定 自分のローカルにて以下を記載(make setupする前の最初のログインのみ、Userがubuntu出ないとログインできないことに注意) `~/.ssh/config` ``` Host isu* IdentityFile ~/.ssh/xxx.pem User isucon Host isu1 HostName xxx.xxx.xxx.xxx # これはpprof用 LocalForward 8090 localhost:8090 # ISUCON11qのようにローカルフォワーディングが必要なときはこんな感じで書くとよい # LocalForward 5000 localhost:5000 Host isu2 HostName xxx.xxx.xxx.xxx Host isu3 HostName xxx.xxx.xxx.xxx ``` ### s1/s2/s3 共通のsetup ``` $ sudo -iu isucon $ wget https://raw.githubusercontent.com/usatie/isucon-setup/main/Makefile ``` s1/s2/s3によってこれだけ変更する ``` $ make set-as-s1 ``` ``` $ sudo systemctl list-units --type=service | grep isu $ vim Makefile ``` これらを変更する ``` USER:=isucon BIN_NAME:=isucondition BUILD_DIR:=$(HOME)/webapp/go SERVICE_NAME:=$(BIN_NAME).go.service ``` ``` $ make setup $ make oh-my-zsh $ make zsh-setup $ exec $SHELL $ vim :Copilot setup :LspInstallServer ``` 一旦exitする Githubにdeploy keyを登録する ``` $ cat ~/.ssh/id_rsa.pub ``` https://github.com/usatie/isucon13f/settings/keys/new もしgolangがデフォルトじゃなかったらgolangに切り替える ``` $ sudo systemctl stop isu-ruby $ sudo systemctl disable isu-ruby $ sudo systemctl start isu-go $ sudo systemctl enable isu-go ``` ### s1だけでやる ``` $ make set-as-s1 ``` Makefileの設定値を編集してcommit/pushする ``` $ vim Makefile ``` 初期設定をcommitする ``` $ make get-conf $ git init && git branch -M main && git add . && git commit -m "Initial commit s1" $ git remote add origin git@github.com:usatie/isucon13f.git $ ggp ``` ### s2/s3 ``` $ make set-as-s2 又は $ make set-as-s3 ``` ``` $ git init && git branch -M main && git remote add origin git@github.com:usatie/isucon13f.git ``` pullするとoverwriteのエラーが出るので、無理やりこれで解決 ``` $ git fetch origin $ git reset --hard FETCH_HEAD ``` その後、初期設定をcommitする ``` $ make get-conf $ git add . && git commit -m "Initial commit $SERVER_ID" $ git push origin main ``` その後、他のサーバーの設定をコピーする ``` $ cp -r s2/etc s3/ $ gss # 確認 $ make deploy-conf $ git add . && git commit -m "Setup $SERVER_ID" $ ggp ``` ## Golang ### Golangのアップデート `make go-setup`に追加したので多分やる必要は出てこないはず ``` $ cd ~/local $ rm -rf go # もし最初からgoが入っていたら削除 $ curl -L https://go.dev/dl/go1.18.3.linux-amd64.tar.gz | tar -zxf - ``` ### main.go(最初に書くこと) ``` import "net/http/pprof" func main () { // 最後に簡単にログを切れるようにコメントを追加しておく // log.SetFlags(0) // log.SetOutput(ioutil.Discard) // おなじみの interpolateParams=true dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local&interpolateParams=true", user, password, host, port, dbname, ) db, err = sqlx.Open("mysql", dsn) // dbのコネクション数の設定 db.SetMaxIdleConns(1000) db.SetMaxOpenConns(1000) // pprofの設定(echo) e := echo.New() pprofGroup := e.Group("/debug/pprof") pprofGroup.Any("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline))) pprofGroup.Any("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile))) pprofGroup.Any("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) pprofGroup.Any("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace))) pprofGroup.Any("/*", echo.WrapHandler(http.HandlerFunc(pprof.Index))) // pprofの設定(goji) mux := goji.NewMux() mux.HandleFunc(pat.Get("/debug/pprof/cmdline"), pprof.Cmdline) mux.HandleFunc(pat.Get("/debug/pprof/profile"), pprof.Profile) mux.HandleFunc(pat.Get("/debug/pprof/symbol"), pprof.Symbol) mux.HandleFunc(pat.Get("/debug/pprof/trace"), pprof.Trace) mux.HandleFunc(pat.Get("/debug/pprof/*"), pprof.Index) } ``` もしかしてpprof、どのパターンでもこれだけでいいかも ``` import ( _ "net/http/pprof" ... ) func main() { runtime.SetBlockProfileRate(1) go func() { log.Fatal(http.ListenAndServe(":6060", nil)) }() } ``` ### /etc/systemd/system/*.service(最初に書くこと) カーネルパラメーターをチューニングするときはこのコマンド ``` # edit時のエディタを変える $ sudo update-alternatives --config editor # editする $ sudo systemctl edit isu-go.service $ sudo systemctl edit isucondition.go.service ``` ``` [Service] LimitNOFILE=65536 ``` ### お役立ち #### echoのリクエストをcurlで実行できる形で出力 ``` func printAsCurl(c echo.Context) error { // リクエストメソッドとURLを取得 method := c.Request().Method url := c.Request().URL.String() // curlコマンドを組み立てる curl := fmt.Sprintf("curl -X %s 'http://localhost%s'", method, url) // リクエストヘッダーを取得してcurlコマンドに追加 for name, headers := range c.Request().Header { for _, h := range headers { curl += fmt.Sprintf(" -H '%s: %s'", name, h) } } // リクエストボディがある場合は、それを取得してcurlコマンドに追加 if c.Request().Body != nil { body, err := ioutil.ReadAll(c.Request().Body) if err != nil { // エラー処理をここに記述します return err } // リクエストボディをcurlコマンドに追加 curl += fmt.Sprintf(" -d '%s'", string(body)) // ボディを読み取ったので、リクエストのBodyを再度読み取れるようにリセットします c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body)) } // 組み立てたcurlコマンドを出力 fmt.Println(curl) return nil } ``` ## NGINX ### 最初にやる設定(etc/nginx/nginx.conf) confファイルを編集して、変更内容をコミットする ``` $ sudo vim /etc/nginx/nginx.conf ``` ``` worker_rlimit_nofile 65536; events { worker_connections 2048; multi_accept on; } http { sendfile on; # NFSでネットワークマウントされてるのじゃなければONがいい tcp_nopush on; # open file cache open_file_cache max=100000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on; # gzip gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_min_length 1k; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; # ssl ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; log_format ltsv "time:$time_local" "\thost:$remote_addr" "\tforwardedfor:$http_x_forwarded_for" "\treq:$request" "\tstatus:$status" "\tmethod:$request_method" "\turi:$request_uri" "\tsize:$body_bytes_sent" "\treferer:$http_referer" "\tua:$http_user_agent" "\treqtime:$request_time" "\tcache:$upstream_http_x_cache" "\truntime:$upstream_http_x_runtime" "\tapptime:$upstream_response_time" "\tvhost:$host"; access_log /var/log/nginx/access.log ltsv; # 終了直前にこちらをコメント外してログを切る # access_log off; } ``` ### 最初にやる設定(/etc/nginx/sites-available/isucondition.cnf) ちなみに`/etc/nginx/sites-available`は非推奨で、`/etc/nginx/conf.d/`が推奨とのことだけど最初からそっちに入ってたら仕方ない。 ``` $ sudo vim /etc/nginx/sites-available/isucondition.cnf ``` DBサーバーを切り分けた場合、初期化エンドポイントはDBサーバーに仕向けた方が楽だったりもした。 ``` upstream app { server localhost:8080; # 2台目のapp serverを追加する時はここに追加(どのサーバーにどれくらい負荷をかけるかのweightも設定可能。nginxと共存してるサーバーと1:2とか2:3くらいにする?) # server xxx.xxx.xxx.xxx:port weight=2; keepalive 64; keepalive_requests 10000; } server { client_max_body_size 10m; location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://app; } } ``` ### カーネルパラメーターをチューニングする ``` $ sudo systemctl edit nginx.service ``` ``` [Service] LimitNOFILE=65536 ``` ### client cache 実質expiresだけでclient側でキャッシュしてくれるようになり、不要なrequestが来なくなる。try_filesは$uriに静的ファイルがあれば配信して、なければappに取りにいくという設定。最初に画像がDBに保存されていた場合など。 ``` server { location ~ /(image|js|css|img)/ { try_files $uri @app; expires 60s; } } ``` ## alp confファイルを編集して、変更内容をコミットする。 `main.go`の中のルーター部分を見るとmatchの部分は書きやすいと思う。 ``` $ vim tool-config/alp/config.yml $ make alp ``` ### 参考パターン ![](https://i.imgur.com/7KqEzvD.png) ``` matching_groups: - /api/isu/[-a-z0-9]+ - /api/isu/.+/icon - /api/isu/.+/graph - /api/condition/[-a-z0-9]+ - /isu/[-a-z0-9]+ - /isu/.+/graph - /isu/.+/condition ``` ![](https://i.imgur.com/INxwwfY.png) ``` matching_groups: - /posts/.+ - /image/.+ - /@[a-zA-Z]+ ``` ## MySQL ### envファイルの確認 ``` MYSQL_HOST:=$(ISUCON_DB_HOST) MYSQL_PORT:=$(ISUCON_DB_PORT) MYSQL_USER:=$(ISUCON_DB_USER) MYSQL_PASS:=$(ISUCON_DB_PASSWORD) MYSQL_DBNAME:=$(ISUCON_DB_NAME) ``` ### 最初にやる設定(slow_query/max_conneciton) confファイルを編集する ``` $ sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf $ make get-db-conf ``` slow-queryが有効化できなかった時(/var/log/mysql/mysql-slow.logの権限問題) ``` $ sudo ls /var/log/mysql/ -la total 8 drwxr-x--- 2 mysql adm 4096 Nov 23 07:42 . drwxr-xr-x 12 root syslog 4096 Nov 23 03:28 .. -rw-r--r-- 1 root root 0 Nov 23 07:44 error.log -rw-r--r-- 1 root root 0 Nov 23 07:44 mysql-slow.log $ sudo chmod 666 /var/log/mysql/mysql-slow.log $ sudo chmod 666 /var/log/mysql/error.log ``` ``` slow_query_log slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 0 log-queries-not-using-indexes open_files_limit = 65536 max_connections = 10000 # DBとappを分離する際に必要 bind-address = 0.0.0.0 # 読み込んだデータ及びインデックスをメモリ上に確保する機能(DB専用サーバーならば物理メモリの80%程度にしておく) # この例は3GB(4GBメモリを想定) innodb_buffer_pool_size = 3G # innodb_buffer_poolを活用する場合は、ディスクキャッシュと二重でメモリを確保しないようにO_DIRECTを設定する innodb_flush_method=O_DIRECT # 更新ログをdisk cacheをストレージに同期させるのを非同期に変更する innodb_flush_log_at_trx_commit = 2 # 更新ログをそもそもオフにする(レプリケーションしないならこれでいい) disable-log-bin = 1 ``` カーネルパラメーターをチューニングするときはこのコマンド ``` $ sudo systemctl edit mysql.service ``` ``` [Service] LimitNOFILE=65536 ``` 参考 https://knowledge.sakura.ad.jp/11981/ ``` [Service] LimitNOFILE=65536 ``` ### DBの水平分割 #### IDの採番(snowflake) snowflakeモジュールのインストール ``` $ cd webapp/go $ go get github.com/bwmarrin/snowflake ``` ソースコードの変更 ``` import ( "github.com/bwmarrin/snowflake" ) // generateID ユニークなIDを生成する var ( snowflakeNode *snowflake.Node ) func initSnowflake() { serverIdStr := os.Getenv("SERVER_ID") // "s1" if serverIdStr == "" { serverIdStr = "s1" } id, err := strconv.ParseInt(serverIdStr[1:], 10, 64) snowflakeNode, err = snowflake.NewNode(id) if err != nil { panic(err) } } func (h *Handler) generateID() (int64, error) { return snowflakeNode.Generate().Int64(), nil } ``` #### sharding用のhash xxhashモジュールのインストール ``` $ cd webapp/go $ go get github.com/cespare/xxhash/v2 ``` ``` import ( "github.com/cespare/xxhash/v2" ) type Handler struct { DB *sqlx.DB DB2 *sqlx.DB DB3 *sqlx.DB DB4 *sqlx.DB } const dbNum = 4 func (h *Handler) getAllDB() []*sqlx.DB { return []*sqlx.DB{h.DB, h.DB2, h.DB3, h.DB4} } func hash(userID int64) uint64 { return xxhash.Sum64String(strconv.FormatInt(userID, 10)) } func (h *Handler) getDB(userID int64) *sqlx.DB { return h.getAllDB()[hash(userID)%dbNum] } func main() { // ... dbx, err := connectDB(false, 1) if err != nil { e.Logger.Fatalf("failed to connect to db: %v", err) } defer dbx.Close() dbx2, err := connectDB(false, 2) if err != nil { e.Logger.Fatalf("failed to connect to db2: %v", err) } defer dbx2.Close() dbx3, err := connectDB(false, 3) if err != nil { e.Logger.Fatalf("failed to connect to db3: %v", err) } defer dbx3.Close() dbx4, err := connectDB(false, 4) if err != nil { e.Logger.Fatalf("failed to connect to db4: %v", err) } defer dbx4.Close() // ... } ``` ### 役立つMySQLコマンド集 #### DB分離直後のテスト Security GroupでPORT3306を開けてあげる必要があることに注意! ``` # DBサーバー以外から $ make access-db ``` #### 許可するhostの変更(DBサーバー分離の時) 許可あるユーザーで入る必要があるのでsudo mysqlが必要 ``` $ sudo mysql mysql> select user, host from mysql.user; mysql> update mysql.user set host='%' where user = 'isucon'; ``` #### connection数の確認 ``` mysql> show variables like "%connection%"; mysql> show status like 'Thread_%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | Threads_cached | 0 | | Threads_connected | 2 | | Threads_created | 2 | | Threads_running | 2 | +-------------------+-------+ mysql> show processlist; +----+-----------------+-----------+---------+---------+-------+------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+-----------------+-----------+---------+---------+-------+------------------------+------------------+ | 5 | event_scheduler | localhost | NULL | Daemon | 32655 | Waiting on empty queue | NULL | | 8 | isuconp | localhost | isuconp | Sleep | 4698 | | NULL | | 10 | isuconp | localhost | isuconp | Query | 0 | init | show processlist | +----+-----------------+-----------+---------+---------+-------+------------------------+------------------+ ``` #### テーブル一覧、テーブル構造一覧の出力など ``` mysql> show DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | isucon | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.00 sec) mysql> show TABLES; +-----------------------------------+ | Tables_in_isucon | +-----------------------------------+ | admin_sessions | | admin_users | | gacha_item_masters | | gacha_masters | | id_generator | | item_masters | | login_bonus_masters | | login_bonus_reward_masters | | present_all_masters | | user_bans | | user_cards | | user_decks | | user_devices | | user_items | | user_login_bonuses | | user_one_time_tokens | | user_present_all_received_history | | user_presents | | user_sessions | | users | | version_masters | +-----------------------------------+ 21 rows in set (0.00 sec) mysql> desc comments; +------------+-----------+------+-----+-------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +------------+-----------+------+-----+-------------------+-------------------+ | id | int | NO | PRI | NULL | auto_increment | | post_id | int | NO | | NULL | | | user_id | int | NO | | NULL | | | comment | text | NO | | NULL | | | created_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | +------------+-----------+------+-----+-------------------+-------------------+ ``` #### テーブル一覧(カラム数の表示) ``` mysql> SELECT TABLE_NAME,TABLE_ROWS,AVG_ROW_LENGTH,DATA_LENGTH,MAX_DATA_LENGTH,INDEX_LENGTH from information_schema.TABLES WHERE table_schem a = 'isucon'; +-----------------------------------+------------+----------------+-------------+-----------------+--------------+ | TABLE_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH | MAX_DATA_LENGTH | INDEX_LENGTH | +-----------------------------------+------------+----------------+-------------+-----------------+--------------+ | admin_sessions | 2 | 8192 | 16384 | 0 | 16384 | | admin_users | 0 | 0 | 16384 | 0 | 0 | | gacha_item_masters | 1012 | 97 | 98304 | 0 | 81920 | | gacha_masters | 40 | 409 | 16384 | 0 | 0 | | id_generator | 1 | 16384 | 16384 | 0 | 0 | | item_masters | 30 | 546 | 16384 | 0 | 0 | | login_bonus_masters | 4 | 4096 | 16384 | 0 | 0 | | login_bonus_reward_masters | 42 | 390 | 16384 | 0 | 0 | | present_all_masters | 29 | 564 | 16384 | 0 | 0 | | user_bans | 3334 | 63 | 212992 | 0 | 147456 | | user_cards | 722962 | 79 | 57245696 | 0 | 36257792 | | user_decks | 10109 | 157 | 1589248 | 0 | 262144 | | user_devices | 20484 | 128 | 2637824 | 0 | 3178496 | | user_items | 133550 | 74 | 9977856 | 0 | 5783552 | | user_login_bonuses | 31220 | 84 | 2637824 | 0 | 1589248 | | user_one_time_tokens | 20 | 819 | 16384 | 0 | 16384 | | user_present_all_received_history | 241360 | 76 | 18399232 | 0 | 0 | | user_presents | 3332730 | 126 | 421511168 | 0 | 141312000 | | user_sessions | 40 | 409 | 16384 | 0 | 16384 | | users | 13813 | 115 | 1589248 | 0 | 0 | | version_masters | 1 | 16384 | 16384 | 0 | 0 | +-----------------------------------+------------+----------------+-------------+-----------------+--------------+ 21 rows in set (0.00 sec) ``` #### SHOW INDEX(すべてのテーブル) ``` SELECT DISTINCT TABLE_NAME, INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'isucon' AND INDEX_NAME != 'PRIMARY'; ``` #### SHOW INDEX ``` mysql> SHOW INDEX FROM user_present_all_received_history; +-----------------------------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | +-----------------------------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | user_present_all_received_history | 0 | PRIMARY | 1 | id | A | 240862 | NULL | NULL | | BTREE | | | YES | NULL | +-----------------------------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ 1 row in set (0.01 sec) ``` #### generated columnの追加 ``` mysql> ALTER TABLE t ADD neg_popularity INT AS (-popularity); mysql> ALTER TABLE t ADD full_name varchar(100) AS (CONCAT(first_name, ' ', last_name)); ``` #### indexの追加 ``` mysql> ALTER TABLE t ADD INDEX idx(col1, col2); mysql> ALTER TABLE t ADD INDEX idx(col1 + col2); mysql> ALTER TABLE t ADD INDEX idx(col(10)); ``` #### Full text indexの追加(フルテキストインデックス) ``` mysql> ALTER TABLE t ADD FULLTEXT INDEX idx(col) WITH PARSER ngram; ``` #### Spatial indexの追加 ``` mysql> ALTER TABLE t ADD point POINT AS (POINT(latitude, longitude)) STORED NOT NULL; mysql> ALTER TABLE t ADD SPATIAL INDEX idx(point); ``` ### おまけ:MySQLのアップデート(5.7 -> 8.0など) ``` $ wget https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb $ sudo dpkg -i mysql-apt-config_0.8.22-1_all.deb $ sudo apt-get update $ sudo apt-get install mysql-server ``` mysqlがrestartにこける場合は、error.logを見て怒られているdirectiveを消せば良い。 ``` $ sudo less /var/log/mysql/error.log ``` ## カーネルパラメーター ``` $ sudo vim /etc/sysctl.cnf ``` 参考 https://knowledge.sakura.ad.jp/11981/#tcp_tw_reuse ``` # default & my config value #net.ipv4.ip_local_port_range=32768 60999 net.ipv4.ip_local_port_range=10511 65535 #net.ipv4.tcp_max_tw_buckets=4096 net.ipv4.tcp_max_tw_buckets=65536 #net.ipv4.tcp_tw_reuse=0 net.ipv4.tcp_tw_reuse=1 ``` ## 典型的な改善アイディアリスト ### indexを貼る(本当に機能しているか?) - ORが入っているとインデックスは効かない - ORをやめてUNIONで繋ぐとか ``` SELECT * FROM users WHERE (name = '田中' OR name = '佐藤') AND age = 30; ``` ``` (SELECT * FROM users WHERE name = '田中' AND age = 30) UNION ALL (SELECT * FROM users WHERE name = '佐藤' AND age = 30); ``` - whereやorder byは順番通りにインデックスを貼ってないと効かない - `score > 80`のような条件が入っているとインデックスは効かない(`=`で条件を指定する必要がある) ### Bulk Insert(N+1を解消する) ``` type User struct { ID int64 `db:"id"` Name string `db:"name"` Age int64. `db: "age"` } func bulkInsert(users []User) { query := "INSERT INTO user (name, age) VALUES (:name, :age)" _, err := db.NamedExec(query, users) if err != nil { fmt.Println("Error %v", err) } } ``` ### 遅延 Insert(buf | timer) 遅延させてinsertを実行するため、エラーをclientには通知できないことに注意。 ``` func postIsuCondition(c echo.Context) error { conditions := []IsuCondition{} err := c.Bind(&conditions) if err != nil || len(req) == 0 { return c.String(http.StatusBadRequest, "bad request body") } // goroutineで遅延insert go func() { bulkInsert(conditions) }() return c.NoContent(http.StatusAccepted) } func bulkInsert(conditions []IsuCondition) { _, err := db.NamedExec( "INSERT INTO `isu_condition`"+ " (`jia_isu_uuid`, `timestamp`, `is_sitting`, `condition`, `message`, `is_dirty`, `is_overweight`, `is_broken`)"+ " VALUES (:jia_isu_uuid, :timestamp, :is_sitting, :condition, :message, :is_dirty, :is_overweight, :is_broken)", conditions, ) if err != nil { c.Logger().Errorf("db error: %v", err) return } } ``` ``` type ConditionBuffer struct { conditions []IsuCondition mu sync.RWMutex } var buffer := ConditionBuffer{} func postIsuCondition(c echo.Context) error { ... buffer.mu.Lock() buffer.conditions = append(buffer.conditions, conditions) buffer.mu.Unlock() // goroutineで遅延insert(100件以上溜まっていれば) go func() { buffer.mu.Lock() if len(buffer.conditions) > 100 { bulkInsert(buffer.conditions) buffer.conditions = []IsuCondition } buffer.mu.Unlock() }() return c.NoContent(http.StatusAccepted) } func main() { // 500ms毎にbulkInsertを実行(1件でもあれば) go func() { for range time.Tick(time.Millisecond * 500) { buffer.mu.Lock() if len(buffer.conditions) > 0 { bulkInsert(buffer.conditions) buffer.conditions = []IsuCondition } buffer.mu.Unlock() } }() } ``` ### DBにカラムの追加(generated column) - 既存のテーブルを壊さずにテーブル構造を変えてインデックスを効くように変更 ``` ALTER TABLE isu_condition ADD COLUMN `is_dirty` bool NOT NULL DEFAULT false, ADD COLUMN `is_overweight` bool NOT NULL DEFAULT false, ADD COLUMN `is_broken` bool NOT NULL DEFAULT false; UPDATE isu_condition set is_dirty = true where `condition` like "%is_dirty=true%"; UPDATE isu_condition set is_overweight = true where `condition` like "%is_overweight=true%"; UPDATE isu_condition set is_broken = true where `condition` like "%is_broken=true%"; ALTER TABLE isu_condition ADD COLUMN `condition_level` tinyint AS (is_dirty + is_overweight + is_broken); ``` 多分これでも動くはず(試していない) ``` ALTER TABLE isu_condition ADD COLUMN `is_dirty` bool AS (condition like '%is_dirty=true%') STORED NOT NULL, ADD COLUMN `is_overweight` bool AS (`condition` like '%is_overweight=true%') STORED NOT NULL, ADD COLUMN `is_broken` bool AS () STORED NOT NULL, ADD COLUMN `condition_level` tinyint AS (is_dirty + is_overweight + is_broken); ``` ### DBにテーブルを追加(サマリーなど) ### cache (リクエスト時の更新) get onlyな値だったらこれでいいが、updateやdeleteもある場合は、その都度SetやRemoveを呼ばないといけないことに注意。 ``` // Cache type Cache struct { m sync.Map ttl time.Duration } func (c *Cache) Get(key any) (any, error) { v, ok := c.m.Load(key) if ok && !v.(*item).expired() { return v.(*item).data, nil } return nil, errors.New(fmt.Sprintf("Cache not found: %v", key)) } func (c *Cache) Set(key any, value any) { c.m.Store(key, &item{ data: value, expireAt: time.Now().Add(c.ttl), }) } func (c *Cache) Remove(key any) { c.m.Delete(key) } // item type item struct { data any expireAt time.Time } func (item *item) expired() bool { return item.expireAt.Before(time.Now()) } ``` ``` var group singleflight.Group var commentCountCache Cache func initCache() { commentCountCache.m = map[string]*item{} commentCountCache.ttl = 1000 * time.Millisecond } func main() { initCache() } func getCommentCount(postID int) (int, error) { // Get cache key := strconv.Itoa(postID) if v, err := commentCountCache.Get(key); err == nil { return v.(int), nil } // update cache (single flight) groupKey := fmt.Sprintf("getCommentCount.%s", key) v, err, _ := group.Do(groupKey, func() (interface{}, error) { var cnt int err := db.Get(&cnt, "SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = ?", postID) if err == nil { commentCountCache.Set(key) } return cnt, err }) if err != nil { return 0, err } return v.(int), nil } ``` ### cache (定期更新) ``` var trendCache sync.Map func initCache() { trendCache = sync.Map{} } func main() { initCache() // 500ms毎にupdateTrendCacheを実行 go func() { for range time.Tick(time.Millisecond * 500) { updateTrendCache() } }() } func postInitialize() { initCache() } func updateTrendCache() { key := "getTrend" // getTrend()が重いとする value := getTrend() trendCache.Store(key, value) } ``` ### 単一の値をSingleflightでキャッシュ これでサッとcacheできるのいいな ``` var gpaCachedAt time.Time var cachedGPAs []float64 var gpaCalcGroup singleflight.Group fung GetGrades() { // ... now := time.Now() var gpas []float64 if now.Sub(gpaCachedAt) > 900*time.Millisecond || cachedGPAs == nil { gpasIf, err, _ := gpaCalcGroup.Do("gpaCalc", func() (interface{}, error) { var newGPAs []float64 q := "SELECT IFNULL(SUM(`submissions`.`score` * `courses`.`credit`), 0) / 100 / `credits`.`credits` AS `gpa`" + " FROM `users`" + " JOIN (" + " SELECT `users`.`id` AS `user_id`, SUM(`courses`.`credit`) AS `credits`" + " FROM `users`" + " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" + " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id` AND `courses`.`status` = ?" + " GROUP BY `users`.`id`" + " ) AS `credits` ON `credits`.`user_id` = `users`.`id`" + " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" + " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id` AND `courses`.`status` = ?" + " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" + " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" + " WHERE `users`.`type` = ?" + " GROUP BY `users`.`id`" if err := h.DB.Select(&newGPAs, q, StatusClosed, StatusClosed, Student); err != nil { return nil, err } return newGPAs, nil }) if err != nil { c.Logger().Error(err) return c.NoContent(http.StatusInternalServerError) } gpas = gpasIf.([]float64) cachedGPAs = gpas gpaCachedAt = now // time.Now() にするとリスク高そうなので保守的に } else { gpas = cachededGPAs } // ... } ``` ### 複数の値をsync.MapでCache(w/ Singleflight) flightGrouptは一個で良くないかと思うけど。 ``` // map by course ID var totalScoreCachedAt = sync.Map{} // map[string]time.Time var cachedTotalScore = sync.Map{} // map[string][]int var totalScoreCalcGroup = sync.Map{} // map[string]*singleflight.Group fung GetGrades() { // ... cachedAt, found := totalScoreCachedAt.Load(course.ID) score, found2 := cachedTotalScore.Load(course.ID) if !found || !found2 || now.Sub(cachedAt.(time.Time)) > 100*time.Millisecond { flightIf, found3 := totalScoreCalcGroup.Load(course.ID) var flight *singleflight.Group if !found3 { flight = &singleflight.Group{} } else { flight = flightIf.(*singleflight.Group) } totalScoreCalcGroup.Store(course.ID, flight) totalsIf, err, _ := flight.Do(fmt.Sprintf("totalScore-%s", course.ID), func() (interface{}, error) { var newTotals []int q := "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" + " FROM `users`" + " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" + " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" + " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" + " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" + " WHERE `courses`.`id` = ?" + " GROUP BY `users`.`id`" if err := h.DB.Select(&newTotals, q, course.ID); err != nil { return nil, err } return newTotals, nil }) if err != nil { c.Logger().Error(err) return c.NoContent(http.StatusInternalServerError) } totals = totalsIf.([]int) cachedTotalScore.Store(course.ID, totals) totalScoreCachedAt.Store(course.ID, now) } else { totals = score.([]int) } // ... } ``` ### nginx cache ``` # メモリは300MBまで使う、60秒でキャッシュの期限は切れる proxy_cache_path /var/cache/nginx keys_zone=CACHE:300m inactive=60s; server { # 初回はproxy_passまで取りに行く # 2回目以降はnginxが直接返す location ~ /api/isu/.*/icon { proxy_pass http://127.0.0.1:3000; proxy_cache CACHE; } } ``` ### client cache クライアント側でcacheしてくれて、そもそもリクエストしてこなくなる。リクエストされても、変更ないですよ〜って返せるようになる。 異なるクライアントは当然リクエストしてくるし、返してあげる必要ある。 ``` server { location ~ /api/isu/.*/icon { # 同じクライアントからの2回目のリクエストが60秒は来ない proxy_pass http://127.0.0.1:3000; expires 60s; } } ``` 静的ファイルがあるかをまず確かめる。あったらそれをnginxガ直接返すし、なければappにリクエストするパターン。 ``` server { location ~ /(image|js|css|img)/ { try_files $uri @app; expires 60s; } location @app { proxy_set_header Host $host; proxy_pass http://app; } } ``` ### N+1を解消する JOINとかで書き換えても時間かかる割にインデックスが効かなくなってしまってあんまりだったりする。 forの中身をcacheするとかが良かったり。 ### LIMITをつける(雑でもいい) 20必要で、でも中には削除済みの投稿もあるからLIMITがついていないとかもある。 LIMITなしだと10000件取ってきちゃう場合、IMIT20にできなくてもLIMIT100でも意味はある。オーダーが変わるならとりあえず雑にでもLIMITつけよう。 ### `SELECT *`をやめる インデックスは効いてるのに、データ量が多くて(バイナリデータが含まれているなど)転送量が多い時(I/Oのせいで遅い時) ### app専用サーバー ### db専用サーバー ### 2台目のdb専用サーバー ### 外部コマンドの書き換え ```diff= import ( + "crypto/sha512" + "encoding/hex" ) func digest(src string) string { - out, err := exec.Command("/bin/bash", "-c", `printf -"%s" `+escapeshellarg(src)+` | openssl dgst -sha512 | -sed 's/^.*= //'`).Output() - if err != nil { - log.Print(err) - return "" - } - - return strings.TrimSuffix(string(out), "\n") + out := sha512.Sum512([]byte(src)) + return hex.EncodeToString(out[:]) } ``` ### 外部コマンドの書き換え(cp, zip) 日本語がファイル名に含まれる場合、自分でheaderを作らないといけなくて少し面倒。 ```diff func createSubmissionsZip(zipFilePath string, classID string, submissions []Submission) error { - tmpDir := AssignmentsDirectory + classID + "/" - if err := exec.Command("rm", "-rf", tmpDir).Run(); err != nil { - return err - } - if err := exec.Command("mkdir", tmpDir).Run(); err != nil { - return err - } - // ファイル名を指定の形式に変更 - for _, submission := range submissions { - if err := exec.Command( - "cp", - AssignmentsDirectory+classID+"-"+submission.UserID+".pdf", - tmpDir+submission.UserCode+"-"+submission.FileName, - ).Run(); err != nil { - return err - } - } - // -i 'tmpDir/*': 空zipを許す - return exec.Command("zip", "-j", "-r", zipFilePath, tmpDir, "-i", tmpDir+"*").Run() + archive, err := os.Create(zipFilePath) + if err != nil { + panic(err) + } + defer archive.Close() + + zw := zip.NewWriter(archive) + defer zw.Close() + + for _, submission := range submissions { + info, err := os.Stat(AssignmentsDirectory + classID + "-" + submission.UserID + ".pdf") + if err != nil { + return err + } + + hdr, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + hdr.Name = submission.UserCode + "-" + submission.FileName + + zh, err := zw.CreateHeader(hdr) + if err != nil { + return err + } + + f, err := os.Open(AssignmentsDirectory + classID + "-" + submission.UserID + ".pdf") + if err != nil { + return err + } + + _, err = io.Copy(zh, f) + if err != nil { + return err + } + f.Close() + } + return nil } ``` ### 外部APIの書き換え ### 外部APIからのデータ取得を、キャッシュしてDBからのDB取得に置き換え ### 複数の外部API呼び出しを非同期で並列処理化 sync.WaitGroupやchanを使って複数のAPIを同時に実行し、待ち合わせをする。 ``` func postBuy() { ... // 1. APIShipmentCreate var scr *APIShipmentCreateRes var scrErr error var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() scr, scrErr = APIShipmentCreate(getShipmentServiceURL(), &APIShipmentCreateReq{ ToAddress: buyer.Address, ToName: buyer.AccountName, FromAddress: seller.Address, FromName: seller.AccountName, }) }() // 2. APIPaymentToken pstr, err := APIPaymentToken(getPaymentServiceURL(), &APIPaymentServiceTokenReq{ ShopID: PaymentServiceIsucariShopID, Token: rb.Token, APIKey: PaymentServiceIsucariAPIKey, Price: targetItem.Price, }) if err != nil { log.Print(err) outputErrorMsg(w, http.StatusInternalServerError, "payment service is failed") tx.Rollback() return } wg.Wait() } ``` ### 静的ファイル化 ``` func writeImage(id int, mime string, data []byte) { var ext string switch mime { case "image/jpeg": ext = ".jpg" case "image/png": ext = ".png" case "image/gif": ext = ".gif" default: fmt.Println("Failed to write file: ", id, mime) return } file_name := fmt.Sprintf("../public/image/%d%s", id, ext) f, err := os.OpenFile(file_name, os.O_WRONLY|os.O_CREATE, 0666) defer f.Close() if err != nil { panic(err) } f.Write(data) } func postIndex(w http.ResponseWriter, r *http.Request) { file, header, err := r.FormFile("file") ... filedata, err := io.ReadAll(file) ... writeImage(int(pid), mime, filedata) } func getImage(w http.ResponseWriter, r *http.Request) { ... err = db.Get(&post, "SELECT * FROM `posts` WHERE `id` = ?", pid) ... writeImage(pid, post.Mime, post.Imgdata) } ``` ### 絞り込みをappからmysqlで行うように変更する - LIMITをつける - whereで条件を絞り込む - カラムを追加する ### templateのコンパイルを一度だけ実行に変更 ``` ``` ### initializeを並列化 sync.WaitGroupやchanを使って複数の処理を同時に実行し、待ち合わせをする。 ``` func initialize(c echo.Context) error { initChan := make(chan int, 2) initf := func (paths []string, data *MySQLConnectionEnv) { for _, p := range paths { sqlFile, _ := filepath.Abs(p) cmdStr := fmt.Sprintf("mysql -h %v -u %v -p%v -P %v %v < %v", data.Host, data.User, data.Password, data.Port, data.DBName, sqlFile, ) if err := exec.Command("bash", "-c", cmdStr).Run(); err != nil { c.Logger().Errorf("Initialize script error : %v", err) initChan <- 1 } } initChan <- 0 } sqlDir := filepath.Join("..", "mysql", "db") go initf( []string{ filepath.Join(sqlDir, "0_Schema.sql"), filepath.Join(sqlDir, "2_DummyChairData.sql"), filepath.Join(sqlDir, "3_AlterTable.sql"), }, mySQLConnectionData) go initf( []string{ filepath.Join(sqlDir, "0_Schema.sql"), filepath.Join(sqlDir, "1_DummyEstateData.sql"), filepath.Join(sqlDir, "3_AlterTable.sql"), }, mySQLConnectionData2) cnt := 0 for { select { case i := <-initChan: if i == 1 { return c.NoContent(http.StatusInternalServerError) } } cnt += 1 if cnt == 2 { break } } return c.JSON(http.StatusOK, InitializeResponse{ Language: "go", }) } ``` ### appログの無効化 ``` func main() { // echoの時 // Loggerは使わない // e.Use(middleware.Logger()) e.Logger.SetLevel(log.FATAL) // いつでも log.SetFlags(0) log.SetOutput(ioutil.Discard) } ``` ### mysqlログの無効化 ### nginxログの無効化 ## おまけ:簡易ベンチの実装方法 ### GET / ``` # 5秒間リクエストを送り続ける $ ab -t 5 http://localhost/ # 1回だけリクエストを送る $ ab -n 1 http://localhost/ $ curl http://localhost/ ``` ### POST /comment `comment.txt` ``` comment=fuga2&post_id=10000&csrf_token=ab201eb2537426de50b1293e6fd6213c&submit=submit ``` dataやcookieはブラウザのインスペクタのNetworkからCopy as cURLでとってくるのがラク ![](https://i.imgur.com/KuHUzXE.png) ``` $ ab -t 5 \ -T "application/x-www-form-urlencoded" \ -C rack.session=8c12f8d877c738e6319cad06b33ca5053a1e045a9767b27b020a24b91d09bfb2 \ -p comment.txt \ http://localhost/comment $ curl 'http://35.77.229.7/comment' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Cookie: rack.session=8c12f8d877c738e6319cad06b33ca5053a1e045a9767b27b020a24b91d09bfb2' \ --data-raw 'comment=fuga2&post_id=10000&csrf_token=ab201eb2537426de50b1293e6fd6213c&submit=submit' ``` ## そのほか使うコマンド ### カーネルパラメーターを確認する ``` $ cat /proc/{pid}/limits ``` ``` $ ps aux | grep mysql mysql 735 0.8 6.0 2570164 471540 ? Ssl Jul18 5:30 /usr/sbin/mysqld $ cat /proc/735/limits ``` ## 初期設定用 ### vim最新版とCopilot vim 最新版のvimをインストールして、Copilot vimを設定する https://itsfoss.com/install-latest-vim-ubuntu/ ``` # nodeが使えるようにPATHを設定 $ vim ~/.zshrc export PATH=$HOME/local/node/bin:$HOME/local/go/bin:$HOME/go/bin:$PATH # Copilot vimをインストール $ git clone https://github.com/github/copilot.vim.git ~/.vim/pack/github/start/copilot.vim # Copilot vimを初期設定 $ vim :Copilot setup ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully