使用Nginx、Go和MongoDB进行REST-WebAPI服务器性能调优第4篇 数据库连接也通过连接池进行

在本文中,我們使用了在AWS EC2上安裝的CentOS 7, nginx 1.10.2和go 1.7。我們還添加了MongoDB 3.4。

连载列表
第一章:根据REST-WebAPI的特点进行调优
第二章:nginx基本调优(仅启用tcp_nopush)
第三章:使用连接池进行后端处理
第四章:通过连接池进行数据库连接

这次我们使用了test服务器 – Web服务器 – ap服务器 – db服务器的配置,并在db服务器上安装了MongoDB 3.4。全部都使用了t2.micro。终于实现了适合连载标题的REST-WebAPI服务器配置(激动不已)。

关于安装MongoDB 3.4版本,我参考了在CentOS7上安装MongoDB 3系列的方法。然后,我设置了一个具有test数据库的dbAdmin和readWrite权限的用户和密码。

1. 准备学习

我們將以前的環境作為基礎,在ap伺服器上建立一個新的應用程式,用於連接到MongoDB。

1.1. 网页服务器

我将修改nginx的default.conf文件。在之前的静态内容返回应用程序中保留连接池以访问api2k,以便进行比较,同时新增api3k。

upstream api2k {
     server 172.31.24.119:8080;
     keepalive 100;
}
upstream api3k {
     server 172.31.24.119:8081;
     keepalive 100;
}

server {

# 中略(元々記載されている内容 ※index.htmlを含む)

    location /api2k/ {
        proxy_pass http://api2k/;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
    location /api3k/ {
        proxy_pass http://api3k/;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

细节虽小但值得注意的是,对于api2k,我对location 的URL描述进行了些许更改。这样似乎能更顺利地将URL传递给应用程序。

なお、このまま動かすと、前回と微妙に異なる、SELinuxに起因するセキュリティエラーが発生します。以下の設定により、動作できるようになります。

# setsebool httpd_can_network_connect on -P

1.2. 应用服务器

关于api2与上次相同。这次重点是新建api3。

API3执行以下操作。

    • POSTリクエストには、リクエストBodyをJSONとして、リクエストURLとセットでmongoDBに書き込み、200 OKを返信する。

既に記録済URLが存在すれば、JSON-Bodyを既存のものに上書きする。
リクエストBodyがJSONでなければ、400 Bad Requestを返信する。

GETリクエストには、リクエストURLに対応するJSONをmongoDBから読み出し、Bodyとして200 OKとともに返信する。

リクエストURLに対応する記録がmongoDBに存在しない場合、404 Not Foundを返信する。

mongoDBとの間に100コネクションをプーリングする。

リクエストに対して、利用するコネクションを乱択する。

以下是由Go语言编写的程序源代码。请参考源代码中的注释。

package main

import (
  "fmt"
  "net/http"
  "time"
  "math/rand"
  "io/ioutil"
  "encoding/json"
  mgo "gopkg.in/mgo.v2"
  "gopkg.in/mgo.v2/bson"
)

// mongoDBとの間のコネクションプーリング数
const maxSessions = 100
// プーリングするコネクションを保持する配列
var session []*mgo.Session

// mongoDBに保存するデータ形式
// POST/GETするJSONの中身は_dataの中にネストして記述
// (構造が不定なので interface{}を使う)
// リクエストURLは_pathに記述
type mongoDoc struct {
  Data interface{} `json:"_data" bson:"_data"`
  Path string `json:"_path" bson:"_path"`
}

// リクエスト処理ハンドラ
func handler(w http.ResponseWriter, r *http.Request) {
  // プリーリングされたどのコネクションを使うかを乱択する
  // なお、乱数はグローバルなものは使わずにローカル生成していることに注意
  COL := session[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(maxSessions)].DB("test").C("COL")
  // GETとPOSTの処理分岐
  var doc mongoDoc
  switch r.Method {
  case "GET": // GET時の処理
    // _pathに指定URLで一致するものがあるか検索
    iter := COL.Find(bson.M{"_path": r.RequestURI}).Iter()
    if iter.Next(&doc) {
      // 一致の場合 _dataの中身のJSONを文字列に変換
      jsonstring, err2 := json.Marshal(doc.Data)
      if err2 != nil {
        panic(err2)
      }
      // JSON文字列をbodyにして返す
      fmt.Fprintf(w, string(jsonstring))
    } else {
      // 不一致の場合は、404 Not Foundを返す
      http.NotFound(w, r)
    }
  case "POST": // POST時の処理
    doc.Path = r.RequestURI
    // リクエストBodyをJSONに変換して_dataに入れる
    jsonstring, _ := ioutil.ReadAll(r.Body)
    json.Unmarshal(jsonstring, &doc.Data)
    if doc.Data != nil {
      // 既に登録済のURLであれば上書き、未登録であれば新規登録する
      _, err := COL.Upsert(bson.M{"_path": doc.Path}, &doc)
      if err != nil {
        panic(err)
      }
    } else {
   // リクエストBodyのJSON化に失敗したら、400 Bad Requestを返す
      w.WriteHeader(http.StatusBadRequest)
    }
  }
}

// ここがメインルーチン
func main() {
  // mongoDBのコネクションを作っておく
  mongoInfo := &mgo.DialInfo{
    Addrs:    []string{"172.31.22.189:27017"},
    Timeout:  20 * time.Second,
    Database: "test",
    Username: "test",
    Password: "test",
    Source: "test",
  }
  session = make([]*mgo.Session, maxSessions)
  for i := 0; i < maxSessions; i++ {
    var err error
    session[i], err = mgo.DialWithInfo(mongoInfo)
    if err != nil {
      panic(err)
    }
  }

  // ポート8081でhttpを待ち受けし、リクエストが来たらハンドラを呼ぶ
  http.HandleFunc("/", handler)
  err := http.ListenAndServe(":8081", nil);
  if err != nil {
      return
  }
}

mongDBとの間のコネクションをプーリングするのは、「MongoDBベンチマーク 1インスタンスで8万insert/秒達成」において、CPUスケールアップの可能性まで考慮して最適であると結論づけているためです。

コネクションの選択は、通常であれば順番にサイクリックに選択していくラウンドロビン方式とするところを、あえて、馴染みがないかもしれませんが、乱数により選択する「乱択方式」としました。これは、ラウンドロビンに必要となるグローバル変数の書き込みロックがもたらす待ちを嫌ったためです。ここについては、今後の連載で、定量的な性能比較をしてみると面白いかもしれません。

为了进行基准测试,我会先进行一次数据的POST。

$ curl http://172.31.20.18/api3k/temp/ -X POST -d '{"temp":25}'

試しにGETすると登録したデータが取得できます。

$ curl http://172.31.20.18/api3k/temp/
{"temp":25}

2. 实验结果

遅延100msを引加したtestサーバでabを実行させます。api3kについては、GETとPOSTの双方を測定します。

$ ab -c 10000 -n 1000000 -r http://172.31.20.18/api2k/
$ ab -c 10000 -n 1000000 -r http://172.31.20.18/api3k/temp/
$ echo -n "{\"temp\":21}" > postfile
$ ab -c 10000 -n 1000000 -p postfile -r http://172.31.20.18/api3k/temp/

測定結果を以下に示します。

アクセス先動作Requests/secエラー数/api2k/静的コンテンツ1,8511,121/api3k/temp/mongoDB-GET1,8791,313/api3k/temp/mongoDB-POST1,867781

关于api2k,理应与上次相同的条件,但是不知道为什么性能大幅度下降了。

さらに、結果を分かりやすくするために、testサーバ〜webサーバのボトルネックの影響を外して、webサーバローカルでabをkeepaliveありでも測定してみます。

$ ab -c 100 -n 1000000 -k http://172.31.20.18/api2k/
$ ab -c 100 -n 1000000 -k http://172.31.20.18/api3k/temp/
$ echo -n "{\"temp\":21}" > postfile
$ ab -c 100 -n 1000000 -p postfile -k http://172.31.20.18/api3k/temp/

以下是测定结果的展示。

アクセス先動作Requests/secエラー数/api2k/静的コンテンツ9,3180/api3k/temp/mongoDB-GET4,4960/api3k/temp/mongoDB-POST4,5540

这个 API2k 的值相比上次稍微下降了一些,但并没有太大的变化。

尽管访问mongoDB比静态内容略慢,但显然测试服务器与网络服务器之间是整体性能的瓶颈。

此外,GET/POST之间没有太大的区别。根据”MongoDB性能测试:单节点每秒8万次插入”的报道,通过对MongoDB进行100个连接池和1个CPU的测试,性能接近每秒1万次访问,因此在大约每秒5千次访问的情况下,读写差异不太明显。

需要补充的是,是POST请求还是PUT请求。

这次在写入方法中选择了POST。然而,严格来说,它的操作具有幂等性(如果已经存在,则将其覆盖,因此无论以相同内容进行多少次POST,结果都不会改变),因此使用PUT可能更合适。

广告
将在 10 秒后关闭
bannerAds