利用協(xié)程提高客戶端并發(fā)性能的嘗試

假設(shè)服務(wù)器端提供無限的服務(wù)能力先紫,客戶端如何達(dá)到最高的QPS骗绕?如果使用多線程來提高并發(fā),線程數(shù)量會成為瓶頸贮泞。是否可以通過協(xié)程來提高客戶端的并發(fā)能力楞慈?下面做了一組測試,分別使用同步啃擦、異步囊蓝、協(xié)程+同步等方式請求百度的首頁,運(yùn)行10s令蛉,對比一下QPS聚霜。

測試環(huán)境

2017年MBP,2核4線程言询,16GB內(nèi)存俯萎。

Maven依賴

<properties>
    <kotlin.version>1.3.11</kotlin.version>
</properties>

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    <version>${kotlin.version}</version>
</dependency>

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-test</artifactId>
    <version>${kotlin.version}</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.1.0</version>
</dependency>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>khttp</groupId>
    <artifactId>khttp</artifactId>
    <version>0.1.0</version>
</dependency>

<dependency>
    <groupId>io.github.rybalkinsd</groupId>
    <artifactId>kohttp</artifactId>
    <version>0.5.0</version>
</dependency>

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.12.1</version>
</dependency>

Kotlin多線程同步

使用Kotlin的khttp發(fā)起同步請求。

import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger

public class TestHttpSyncClient {

    val api = "https://www.baidu.com"

    val completedCount = AtomicInteger(0)
    var failedCount = AtomicInteger(0)

    @Test
    fun test() {

        for (i in 0..80) {

            Thread() {

                while (true) {

                    try {

                        var response = khttp.get(api)

                        if (response.statusCode == 200) {
                            completedCount.incrementAndGet()
                        } else {
                            failedCount.incrementAndGet()
                        }
                    }catch (e : Exception) {

                        failedCount.incrementAndGet()
                    }
                }
            }.start()
        }

        Thread.sleep(10 * 1000)
        println("completedCount: ${completedCount}, failedCount: ${failedCount}");
        System.exit(0);
    }
}

線程開到80個(gè)运杭,QPS可達(dá)2600左右夫啊。

completedCount: 26089, failedCount: 111

HttpAsyncClients異步

使用HttpAsyncClients發(fā)起異步請求。

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.junit.Test;

import java.util.concurrent.atomic.AtomicInteger;

public class TestHttpAsyncClient implements Runnable {

   public final static AtomicInteger completedCount = new AtomicInteger(0);
   public final static AtomicInteger failedCount = new AtomicInteger(0);
   public final static AtomicInteger canceledCount = new AtomicInteger(0);

   @Test
   public void test() {

       //啟動線程
       TestHttpAsyncClient testHttpAsyncClient = new TestHttpAsyncClient();
       new Thread(testHttpAsyncClient).start();

       //測試10s
       try {

           Thread.sleep(10 * 1000);
           System.out.println(String.format("completedCount: %d, failedCount: %d, canceledCount: %d",
                   completedCount.get(), failedCount.get(), canceledCount.get()));

           System.exit(0);
       } catch (Exception e) {

           System.err.println(String.format("System.exit exception: %s", e));
       }
   }

   public void run() {

       CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
               .setMaxConnTotal(128)
               .setMaxConnPerRoute(128)
               .build();
       httpclient.start();

       final HttpGet request = new HttpGet("https://www.baidu.com");

       while (true) {

           httpclient.execute(request, new FutureCallback<HttpResponse>() {

               @Override
               public void completed(HttpResponse httpResponse) {

                   if (httpResponse.getStatusLine().getStatusCode() == 200) {
                       completedCount.incrementAndGet();
                   }else {
                       failedCount.incrementAndGet();
                 }
               }

               @Override
               public void failed(Exception e) {
                   failedCount.incrementAndGet();
               }

               @Override
               public void cancelled() {
                   canceledCount.incrementAndGet();
               }
           });
       }
   }
}

并發(fā)設(shè)置為128辆憔,QPS可以達(dá)到2000左右撇眯。

completedCount: 19319, failedCount: 125, canceledCount: 0

通過調(diào)試可以發(fā)現(xiàn)报嵌,只需要4個(gè)dispatcher線程,就可以滿足需要熊榛。

image.png

Kotlin協(xié)程+khttp

使用Kotlin Coroutine+khttp同步請求測試锚国。

import khttp.responses.Response
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicInteger
import org.junit.Test

public class TestKotlinCoroutine {

    val api = "https://www.baidu.com"
    val completedCount = AtomicInteger(0)
    val failedCount = AtomicInteger(0)

    @Test
    fun test() {

        Thread(){

            while (true) {

                val resp =
                        GlobalScope.launch {

                            val result: Deferred<Response> = async {

                                get()
                            }

                            result.await()
                        }
            }
        }.start()

        Thread.sleep(10 * 1000)
        println("completedCount: ${completedCount}, failedCount: ${failedCount}");
        System.exit(0);
    }

    suspend fun get(): khttp.responses.Response {

        var response = khttp.get(api)

        if (response.statusCode == 200) {
            completedCount.addAndGet(1)
        }else {
            failedCount.addAndGet(1)
        }

        return response;
    }
}

協(xié)程版本的性能甚至不如同步版本,QPS只有50玄坦。換一種方式血筑,起多個(gè)協(xié)程,每個(gè)線程里面循環(huán)請求煎楣,QPS也一樣只有50左右豺总。

completedCount: 498, failedCount: 0

這個(gè)版本性能很差,通過調(diào)試可以發(fā)現(xiàn)择懂,最多也就啟動了4個(gè)worker線程喻喳,并且khttp又不支持協(xié)程阻塞。

image.png

下面是一個(gè)優(yōu)化的版本困曙,將工作放到Dispatchers.IO里面去做表伦,盡量多起一些worker線程。

public class TestKotlinCoroutineClient {

    val api = "https://www.baidu.com"

    val completedCount = AtomicInteger(0)
    var failedCount = AtomicInteger(0)
    val time = 10000

    @Test
    fun test() = runBlocking {

        val start = System.currentTimeMillis()
        val channel = Channel<Int>()

        repeat(time) {
            launch { channel.send(get()) }
        }

        repeat(time) {
            val code = channel.receive()
            if (code == 200) {
                completedCount.incrementAndGet()
            } else {
                failedCount.incrementAndGet()
            }
        }

        val end = System.currentTimeMillis()
        println("completedCount: ${completedCount}, failedCount: ${failedCount}, cost${(end - start) / 1000}");
        System.exit(0);
    }

    suspend fun get() = withContext(Dispatchers.IO) { khttp.get(api) }.statusCode
}

性能可以提升到1200左右慷丽。調(diào)試可以發(fā)現(xiàn)線程數(shù)量最多可達(dá)68個(gè)蹦哼。

image.png

Kotlin協(xié)程+kohttp

kohttp看起來支持協(xié)程。使用kohttp請求一萬次盈魁,花費(fèi)時(shí)間100s左右翔怎,QPS為100窃诉。

import io.github.rybalkinsd.kohttp.ext.asyncHttpGet
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.system.measureTimeMillis

public class TestKotlinCoroutine {

    val api = "https://www.baidu.com"

    @Test
    fun `many async invokes of httpGet`() {
        measureTimeMillis {
            GlobalScope.launch {
                val tasks = List(10000) {
                    api.asyncHttpGet()
                }
                tasks.map { r ->
                    r.await().also { it.close() }
                }.forEach {
                    assertEquals(200, it.code())
                }
            }
        }.also { println("$it ms") }
    }
}

分析一下kohttp的實(shí)現(xiàn)杨耙,可以發(fā)現(xiàn)它只是封裝了okhttp3,使用了異步模式飘痛。okhttp3本身不支持協(xié)程珊膜,所以性能也不會太好。

fun String.asyncHttpGet(client: Call.Factory = defaultHttpClient): Deferred<Response> =
    GlobalScope.async(context = Unconfined) {
        client.suspendCall(Request.Builder().url(this@asyncHttpGet).build())
    }

internal suspend fun Call.Factory.suspendCall(request: Request): Response =
    suspendCoroutine { cont ->
        newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                cont.resume(response)
            }

            override fun onFailure(call: Call, e: IOException) {
                cont.resumeWithException(e)
            }
        })
    }

Go協(xié)程

順便試一下Go協(xié)程的性能宣脉。Go協(xié)程版本在使用keepalive的情況下车柠,256個(gè)協(xié)程的QPS可以達(dá)到1700左右。注意需要讀一下resp.Body塑猖,這樣keepalive才會生效竹祷。參考文章:A brief intro of TCP keep-alive in Go’s HTTP implementation

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "runtime"
    "sync"
    "sync/atomic"
    "time"
)

var completedCount uint64
var failedCount uint64
var lock = &sync.Mutex{}

func main() {

    runtime.GOMAXPROCS(4)
    for i := 0; i < 256; i++ {
        clt := newQPSClient()
        go clt.test()
    }

    time.Sleep(10 * time.Second)
    fmt.Printf("completedCount: %d, failedCount: %d\n", completedCount, failedCount)
    os.Exit(0)
}

type QPSClient struct {
    clt *http.Client
}

func newQPSClient() *QPSClient {
    return &QPSClient{
        clt: &http.Client{},
    }
}

func (qc *QPSClient) test() {

    for {
        resp, err := qc.clt.Get("https://www.baidu.com")
        if err == nil && (resp.StatusCode == 200) {

            _, err = io.Copy(ioutil.Discard, resp.Body)
            resp.Body.Close()
            atomic.AddUint64(&completedCount, 1)
        } else {

            _, err = io.Copy(ioutil.Discard, resp.Body)
            atomic.AddUint64(&failedCount, 1)
        }
    }
}

結(jié)論

請求方式 QPS
Kotlin多線程同步 80個(gè)線程羊苟,QPS 2600
HttpAsyncClients異步 最大128個(gè)連接塑陵,QPS 2000
Kotlin協(xié)程+khttp 循環(huán)起協(xié)程+khttp同步,QPS 50
Kotlin協(xié)程+kohttp 循環(huán)起協(xié)程+okhttp3異步蜡励,QPS 100
Go協(xié)程 256個(gè)協(xié)程令花,QPS 1700

上面這些測試阻桅,缺乏更精細(xì)的設(shè)置,比如HttpAsyncClients 128個(gè)連接使用了多少個(gè)線程兼都?Kotlin和Go的協(xié)程版本使用了多少線程嫂沉?還有沒有提升空間?

Coroutine運(yùn)行有很多受限條件扮碧,不能堵塞在操作系統(tǒng)會堵塞線程的地方趟章,需要自己實(shí)現(xiàn)對這些堵塞API的處理,在用戶態(tài)做上下文的保存和恢復(fù)慎王。Java生態(tài)圈中缺乏對協(xié)程支持良好的基礎(chǔ)庫尤揣,導(dǎo)致不能發(fā)揮協(xié)程真正的威力,使用異步IO是更好的解決辦法柬祠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末北戏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子漫蛔,更是在濱河造成了極大的恐慌嗜愈,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莽龟,死亡現(xiàn)場離奇詭異蠕嫁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)毯盈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門剃毒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人搂赋,你說我怎么就攤上這事赘阀。” “怎么了脑奠?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵基公,是天一觀的道長。 經(jīng)常有香客問我宋欺,道長轰豆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任齿诞,我火速辦了婚禮酸休,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祷杈。我一直安慰自己斑司,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布吠式。 她就那樣靜靜地躺著陡厘,像睡著了一般抽米。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糙置,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天云茸,我揣著相機(jī)與錄音,去河邊找鬼谤饭。 笑死标捺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的揉抵。 我是一名探鬼主播亡容,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冤今!你這毒婦竟也來了闺兢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤戏罢,失蹤者是張志新(化名)和其女友劉穎屋谭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龟糕,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桐磁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讲岁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片我擂。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缓艳,靈堂內(nèi)的尸體忽然破棺而出校摩,到底是詐尸還是另有隱情,我是刑警寧澤郎任,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布秧耗,位于F島的核電站备籽,受9級特大地震影響舶治,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜车猬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一霉猛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧珠闰,春花似錦惜浅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伐厌。三九已至,卻和暖如春裸影,著一層夾襖步出監(jiān)牢的瞬間挣轨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工轩猩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留卷扮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓均践,卻偏偏與公主長得像晤锹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子彤委,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容