すべてのプロダクト
Search
ドキュメントセンター

Alibaba Cloud Model Studio:CosyVoice の高同時実行性シナリオ

最終更新日:Feb 11, 2026

CosyVoice の音声合成は、ストリーミングリアルタイム通信をサポートするため WebSocket プロトコルを使用します。ただし、高い同時実行性のシナリオでは、各リクエストごとに WebSocket 接続を作成・破棄すると、ネットワークおよびシステムリソースへの負荷が大きく、接続遅延も顕著になります。パフォーマンスを最適化し安定性を確保するため、DashScope SDK には接続プールやオブジェクトプールなど、効率的なリソース再利用メカニズムが組み込まれています。本ドキュメントでは、DashScope Python SDK および Java SDK を使用して、高い同時実行性条件下で CosyVoice を効率的に呼び出す方法について説明します。

前提条件

Python SDK:オブジェクトプールによる最適化

Python SDK では、SpeechSynthesizerObjectPool を使用してオブジェクトプールの最適化を実装します。これにより、SpeechSynthesizer オブジェクトの管理および再利用が可能になります。

初期化時に、オブジェクトプールは指定された数の SpeechSynthesizer インスタンスを作成し、WebSocket 接続を事前に確立します。プールからオブジェクトを取得すると、新規接続の確立にかかる時間が発生しないため、即座にリクエストを送信できます。これにより、最初のパッケージ遅延(first-package latency)が低減されます。タスク終了後にオブジェクトをプールへ返却すると、その WebSocket 接続はオープン状態のまま保持され、再利用可能になります。

実装手順

  1. 依存関係のインストール:DashScope パッケージをインストールします(pip install -U dashscope)。

  2. オブジェクトプールの作成および構成。

    SpeechSynthesizerObjectPool を使用してプールサイズを設定します。推奨値は、ピーク同時実行数の 1.5 倍~2 倍です。プールサイズは、ご利用のアカウントのクエリ/秒(QPS)制限を超えないように設定してください。

    以下のコードを使用して、グローバルなシングルトン固定サイズオブジェクトプールを作成します。このプールは初期化時に指定された数の SpeechSynthesizer オブジェクトを作成し、WebSocket 接続を確立します。この処理には時間がかかります。

    from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool
    
    synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
  3. プールから SpeechSynthesizer オブジェクトを取得。

    現在借用中のオブジェクト数がプールの最大サイズを超える場合、システムは新しい SpeechSynthesizer オブジェクトを作成します。

    この新しく作成されたオブジェクトは、初期化および WebSocket 接続の確立を行う必要があります。既存のプール内の接続を再利用できないため、プールによる恩恵を受けません。

    speech_synthesizer = connectionPool.borrow_synthesizer(
        model='cosyvoice-v3-flash',
        voice='longanyang',
        seed=12382,
        callback=synthesizer_callback
    )
  4. 音声合成の実行。

    SpeechSynthesizer オブジェクトの call または streaming_call メソッドを呼び出して、音声を合成します。

  5. SpeechSynthesizer オブジェクトの返却。

    音声合成タスクが終了したら、後続のタスクで再利用できるよう、SpeechSynthesizer オブジェクトを返却します。

    未完了または失敗したタスクからオブジェクトを返却しないでください。

    connectionPool.return_synthesizer(speech_synthesizer)

完全なコード例

# !/usr/bin/env python3
# Copyright (C) Alibaba Group. All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)

import os
import time
import threading

import dashscope
from dashscope.audio.tts_v2 import *


USE_CONNECTION_POOL = True
text_to_synthesize = [
    '最初の文:Alibaba Cloud の音声合成へようこそ。',
    '2 番目の文:Alibaba Cloud の音声合成へようこそ。',
    '3 番目の文:Alibaba Cloud の音声合成へようこそ。',
]
connectionPool = None
if USE_CONNECTION_POOL:
    print('接続プールを作成中')
    start_time = time.time() * 1000
    connectionPool = SpeechSynthesizerObjectPool(max_size=3)
    end_time = time.time() * 1000
    print('接続プールの作成完了。所要時間:{} ms'.format(end_time - start_time))

def init_dashscope_api_key():
    '''
    DashScope の API キーを設定します。詳細については、以下をご参照ください:
    https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    '''
    # API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得方法:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # 環境変数 DASHSCOPE_API_KEY から API キーを読み込みます
    else:
        dashscope.api_key = '<your-dashscope-api-key>'  # 手動で API キーを設定します


def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
    global USE_CONNECTION_POOL, connectionPool
    '''
    ストリーミングモードで指定されたテキストを音声合成し、非同期で呼び出し、合成された音声をリアルタイムで再生します。
    詳細については、https://www.alibabacloud.com/help/document_detail/2712523.html をご参照ください。
    '''

    complete_event = threading.Event()

    # 結果を処理するコールバックを定義

    class Callback(ResultCallback):
        def on_open(self):
            # オブジェクトプールを使用する場合、on_open はタスク開始後に呼び出されます
            self.file = open(f'result_{task_id}.mp3', 'wb')
            print(f'[task_{task_id}] 開始')

        def on_complete(self):
            print(f'[task_{task_id}] 音声合成タスクが正常に完了しました。')
            complete_event.set()

        def on_error(self, message: str):
            print(f'[task_{task_id}] 音声合成タスクが失敗しました。{message}')

        def on_close(self):
            # オブジェクトプールを使用する場合、on_open はタスク終了後に呼び出されます
            print(f'[task_{task_id}] 終了')

        def on_event(self, message):
            # print(f'recv speech synthsis message {message}')
            pass

        def on_data(self, data: bytes) -> None:
            # プレーヤーへ送信
            # 音声をファイルに保存
            self.file.write(data)

    # 音声合成コールバックを呼び出します
    synthesizer_callback = Callback()

    # 音声合成器を初期化します
    # voice、フォーマット、サンプルレートなどの合成パラメーターをカスタマイズできます
    if USE_CONNECTION_POOL:
        speech_synthesizer = connectionPool.borrow_synthesizer(
            model='cosyvoice-v3-flash',
            voice='longanyang',
            seed=12382,
            callback=synthesizer_callback
        )
    else:
        speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
                                               voice='longanyang',
                                               seed=12382,
                                               callback=synthesizer_callback)
    try:
        speech_synthesizer.call(text)
    except Exception as e:
        print(f'[task_{task_id}] 音声合成タスクが失敗しました。{e}')
        if USE_CONNECTION_POOL:
            # オブジェクトプールを使用中にタスクが失敗した場合は、合成器の接続を手動で閉じます。
            speech_synthesizer.close()
        return

    print('[task_{}] 合成されたテキスト:{}'.format(task_id, text))
    complete_event.wait()
    print('[task_{}][メトリック] requestId:{}、最初のパッケージ遅延(ms):{}'.format(
        task_id,
        speech_synthesizer.get_last_request_id(),
        speech_synthesizer.get_first_package_delay()))
    if USE_CONNECTION_POOL:
        connectionPool.return_synthesizer(speech_synthesizer)


# メイン関数
if __name__ == '__main__':
    # 以下の URL はシンガポールリージョン向けです。北京リージョンのモデルを使用する場合は、代わりに wss://dashscope.aliyuncs.com/api-ws/v1/inference を使用してください。
    dashscope.base_websocket_api_url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference'
    init_dashscope_api_key()
    task_thread_list = []
    for task_id in range(3):
        thread = threading.Thread(
            target=synthesis_text_to_speech_and_play_by_streaming_mode,
            args=(text_to_synthesize[task_id], task_id))
        task_thread_list.append(thread)

    for task_thread in task_thread_list:
        task_thread.start()

    for task_thread in task_thread_list:
        task_thread.join()

    if USE_CONNECTION_POOL:
        connectionPool.shutdown()

リソース管理およびエラー処理

  • タスク成功時:音声合成タスクが正常に完了した場合、connectionPool.return_synthesizer(speech_synthesizer) を呼び出して、SpeechSynthesizer オブジェクトをプールへ返却し、再利用可能にします。

    重要

    未完了または失敗したタスクから SpeechSynthesizer オブジェクトを返却しないでください。

  • タスク失敗時:SDK またはビジネスロジックからの例外によってタスクが中断された場合、基盤となる WebSocket 接続を手動で閉じる必要があります:speech_synthesizer.close()

  • すべての音声合成タスクが終了したら、connectionPool.shutdown() を使用してオブジェクトプールをシャットダウンします。

  • サービスが TaskFailed エラーを返した場合、追加のアクションは不要です。

Java SDK:接続プールおよびオブジェクトプールによる最適化

Java SDK では、内部接続プールとカスタムオブジェクトプールを組み合わせることで、最適なパフォーマンスを実現します。

  • 接続プール:SDK は OkHttp3 の組み込み接続プールを使用して、基盤となる WebSocket 接続の管理および再利用を行います。これによりハンドシェイクのオーバーヘッドが削減され、デフォルトで有効になっています。

  • オブジェクトプール:commons-pool2 を基盤として構築されており、事前に接続が確立された SpeechSynthesizer オブジェクトのグループを維持します。プールからオブジェクトを取得することで、接続確立の遅延が解消され、最初のパッケージ遅延(first-package latency)が大幅に低減されます。

実装手順

  1. 依存関係の追加。

    ビルドツールに応じて、プロジェクトの依存関係設定ファイルに dashscope-sdk-java および commons-pool2 を追加します。

    Maven および Gradle の例:

    Maven

    1. Maven プロジェクトの pom.xml ファイルを開きます。

    2. <dependencies> タグ内に、以下の依存関係を追加します。

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <!-- 'the-latest-version' をバージョン 2.16.9 以降に置き換えます。利用可能なバージョンは、https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java をご確認ください。-->
        <version>the-latest-version</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <!-- 'the-latest-version' を最新バージョンに置き換えます。利用可能なバージョンは、https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 をご確認ください。-->
        <version>the-latest-version</version>
    </dependency>
    1. pom.xml ファイルを保存します。

    2. プロジェクトの依存関係を更新するために、mvn clean install または mvn compile などの Maven コマンドを実行します。

    Gradle

    1. Gradle プロジェクトの build.gradle ファイルを開きます。

    2. dependencies ブロック内に、以下の依存関係を追加します。

      dependencies {
          // 'the-latest-version' をバージョン 2.16.6 以降に置き換えます。利用可能なバージョンは、https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java をご確認ください。
          implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version'
          
          // 'the-latest-version' を最新バージョンに置き換えます。利用可能なバージョンは、https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 をご確認ください。
          implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version'
      }
    3. build.gradle ファイルを保存します。

    4. ターミナルでプロジェクトのルートディレクトリに移動し、依存関係を更新するための以下の Gradle コマンドを実行します。

      ./gradlew build --refresh-dependencies

      Windows を使用している場合は、以下のコマンドを実行します。

      gradlew build --refresh-dependencies
  2. 接続プールの構成。

    環境変数を使用して、主要な接続プールパラメーターを設定します。

    環境変数

    説明

    DASHSCOPE_CONNECTION_POOL_SIZE

    接続プールのサイズ。

    推奨値:ピーク同時実行数の 2 倍以上。

    デフォルト:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    非同期リクエストの最大数。

    推奨値:DASHSCOPE_CONNECTION_POOL_SIZE と一致させる。

    デフォルト:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    ホストごとの非同期リクエストの最大数。

    推奨値:DASHSCOPE_CONNECTION_POOL_SIZE と一致させる。

    デフォルト:32。

  3. オブジェクトプールの構成。

    環境変数を使用してオブジェクトプールのサイズを設定します。

    環境変数

    説明

    COSYVOICE_OBJECTPOOL_SIZE

    オブジェクトプールのサイズ。

    推奨値:ピーク同時実行数の 1.5 倍~2 倍。

    デフォルト:500。

    重要
    • オブジェクトプールのサイズ(COSYVOICE_OBJECTPOOL_SIZE)は、接続プールのサイズ(DASHSCOPE_CONNECTION_POOL_SIZE)以下である必要があります。そうでない場合、接続プールが満杯の状態でオブジェクトプールがオブジェクトを要求すると、呼び出しスレッドが利用可能な接続が解放されるまでブロックされます。

    • オブジェクトプールのサイズは、ご利用のアカウントの QPS(クエリ/秒)制限を超えてはいけません。

    以下のコードを使用してオブジェクトプールを作成します。

    class CosyvoiceObjectPool {
        // ・・・他のコードは省略。完全な例は以下を参照してください。
        public static GenericObjectPool<SpeechSynthesizer> getInstance() {
            lock.lock();
            if (synthesizerPool == null) {
                // オブジェクトプールのサイズをここで設定できます。または COSYVOICE_OBJECTPOOL_SIZE 環境変数で設定することも可能です。
                // 推奨値:サーバーの最大同時接続数の 1.5 倍~2 倍。
                int objectPoolSize = getObjectivePoolSize();
                SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
                        new SpeechSynthesizerObjectFactory();
                GenericObjectPoolConfig<SpeechSynthesizer> config =
                        new GenericObjectPoolConfig<>();
                config.setMaxTotal(objectPoolSize);
                config.setMaxIdle(objectPoolSize);
                config.setMinIdle(objectPoolSize);
                synthesizerPool =
                        new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
            }
            lock.unlock();
            return synthesizerPool;
        }
    }
  4. プールから SpeechSynthesizer オブジェクトを取得。

    現在借用中のオブジェクト数がプールの最大サイズを超える場合、システムは新しい SpeechSynthesizer オブジェクトを作成します。

    この新しく作成されたオブジェクトは、初期化および WebSocket 接続の確立を行う必要があります。既存のプール内の接続を再利用できないため、プールによる恩恵を受けません。

    synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
  5. 音声合成の実行。

    音声合成を実行するには、SpeechSynthesizer オブジェクトの call または streamingCall メソッドを呼び出すことができます。

  6. SpeechSynthesizer オブジェクトの返却。

    音声合成タスクが終了したら、後続のタスクで再利用できるよう、SpeechSynthesizer オブジェクトを返却します。

    未完了または失敗したタスクからオブジェクトを返却しないでください。

    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);

完全なコード例

import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * プロジェクトには、org.apache.commons.pool2 および DashScope パッケージを含める必要があります。
 *
 * DashScope SDK バージョン 2.16.6 以降は、高い同時実行性のシナリオ向けに最適化されています。
 * DashScope SDK バージョン 2.16.6 より前のバージョンは、高い同時実行性での使用は推奨されません。
 *
 *
 * TTS サービスに対する高い同時実行性の呼び出しを行う前に、
 * 次の環境変数を使用して接続プールのパラメーターを構成してください:
 *
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
 * DASHSCOPE_CONNECTION_POOL_SIZE
 *
 */

class SpeechSynthesizerObjectFactory
        extends BasePooledObjectFactory<SpeechSynthesizer> {
    public SpeechSynthesizerObjectFactory() {
        super();
    }
    @Override
    public SpeechSynthesizer create() throws Exception {
        return new SpeechSynthesizer();
    }

    @Override
    public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
        return new DefaultPooledObject<>(obj);
    }
}

class CosyvoiceObjectPool {
    public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
    public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
    public static int DEFAULT_OBJECT_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
            System.out.println("環境変数からオブジェクトプールサイズを取得: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("デフォルトのオブジェクトプールサイズを使用: "+ DEFAULT_OBJECT_POOL_SIZE);
            return DEFAULT_OBJECT_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // オブジェクトプールサイズをここで設定できます。または COSYVOICE_OBJECTPOOL_SIZE 環境変数で設定することも可能です。
            // 推奨値:サーバーの最大同時接続数の 1.5 倍~2 倍。
            int objectPoolSize = getObjectivePoolSize();
            SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
                    new SpeechSynthesizerObjectFactory();
            GenericObjectPoolConfig<SpeechSynthesizer> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            synthesizerPool =
                    new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
        }
        lock.unlock();
        return synthesizerPool;
    }
}

class SynthesizeTaskWithCallback implements Runnable {
    String[] textArray;
    String requestId;
    long timeCost;
    public SynthesizeTaskWithCallback(String[] textArray) {
        this.textArray = textArray;
    }
    @Override
    public void run() {
        SpeechSynthesizer synthesizer = null;
        long startTime = System.currentTimeMillis();
        // if recv onError
        final boolean[] hasError = {false};
        try {
            class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
                ReactCallback() {}

                @Override
                public void onEvent(SpeechSynthesisResult message) {
                    if (message.getAudioFrame() != null) {
                        try {
                            byte[] bytesArray = message.getAudioFrame().array();
                            System.out.println("受信した音声。音声ストリーム長: " + bytesArray.length);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

                @Override
                public void onComplete() {}

                @Override
                public void onError(Exception e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                    hasError[0] = true;
                }
            }

            // your-dashscope-api-key をご自身の API キーに置き換えます
            String dashScopeApiKey = "your-dashscope-api-key";

            SpeechSynthesisParam param =
                    SpeechSynthesisParam.builder()
                            .model("cosyvoice-v3-flash")
                            .voice("longanyang")
                            // API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得方法:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                            // 環境変数を設定していない場合は、以下の行を .apiKey("sk-xxx") に置き換えてください。
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .format(SpeechSynthesisAudioFormat
                                    .MP3_22050HZ_MONO_256KBPS) // ストリーミング合成には PCM または MP3 を使用します
                            .apiKey(dashScopeApiKey)
                            .build();

            try {
                synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
                synthesizer.updateParamAndCallback(param, new ReactCallback());
                for (String text : textArray) {
                    synthesizer.streamingCall(text);
                }
                Thread.sleep(20);
                synthesizer.streamingComplete(60000);
                requestId = synthesizer.getLastRequestId();
            } catch (Exception e) {
                System.out.println("例外 e: " + e.toString());
                hasError[0] = true;
            }
        } catch (Exception e) {
            hasError[0] = true;
            throw new RuntimeException(e);
        }
        if (synthesizer != null) {
            try {
                if (hasError[0] == true) {
                    // エラーが発生した場合、接続を閉じ、プール内のオブジェクトを無効化します。
                    synthesizer.getDuplexApi().close(1000, "bye");
                    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
                } else {
                    // タスクが正常に完了した場合、オブジェクトを返却します。
                    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[スレッド " + Thread.currentThread() + "] 音声合成タスクが完了しました。所要時間: " + timeCost + " ms、RequestId: " + requestId);
        }
    }
}

@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
    public static void checkoutEnv(String envName, int defaultSize) {
        if (System.getenv(envName) != null) {
            System.out.println("[ENV CHECK]: " + envName + " "
                    + System.getenv(envName));
        } else {
            System.out.println("[ENV CHECK]: " + envName
                    + " デフォルト値(" + defaultSize + ")を使用します");
        }
    }

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {
        // 以下の URL はシンガポールリージョン向けです。北京リージョンのモデルを使用する場合は、代わりに wss://dashscope.aliyuncs.com/api-ws/v1/inference を使用してください。
        Constants.baseWebsocketApiUrl = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference";
        // 接続プールの環境変数を確認
        checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
        checkoutEnv(CosyvoiceObjectPool.COSYVOICE_OBJECTPOOL_SIZE_ENV, CosyvoiceObjectPool.DEFAULT_OBJECT_POOL_SIZE);

        int runTimes = 3;
        // SpeechSynthesis オブジェクトのプールを作成
        ExecutorService executorService = Executors.newFixedThreadPool(runTimes);

        for (int i = 0; i < runTimes; i++) {
            // タスク送信時刻を記録
            LocalDateTime submissionTime = LocalDateTime.now();
            executorService.submit(new SynthesizeTaskWithCallback(new String[] {
                    "私の床の前で、月光が輝いている。", "まるで地上に霜が降りているようだ。", "私は目を上げて明るい月を眺める。", "それから頭を下げ、故郷を思い出す。"}));
        }

        // ExecutorService をシャットダウンし、すべてのタスクが完了するのを待ちます
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

推奨構成

以下の構成は、指定された仕様の Alibaba Cloud ECS インスタンス上で CosyVoice 音声合成サービスのみを実行した際のテスト結果に基づいています。過剰な同時リクエストはタスク遅延を引き起こす可能性があります。

単一マシン同時実行数とは、1 台のマシン上で同時に実行される CosyVoice 音声合成タスクの数を指します。これはワーカースレッドの数と等価です。

マシン構成(Alibaba Cloud)

最大単一マシン同時実行数

オブジェクトプールサイズ

接続プールサイズ

4 vCPU、8 GiB メモリ

100

500

2000

8 vCPU、16 GiB メモリ

150

500

2000

16 vCPU、32 GiB メモリ

200

500

2000

リソース管理およびエラー処理

  • タスク成功時:音声合成タスクが正常に完了した場合、SpeechSynthesizer オブジェクトの GenericObjectPoolreturnObject メソッドを呼び出して、プールへ返却し、再利用可能にします。

    現在のコードでは、これに対応する呼び出しは CosyvoiceObjectPool.getInstance().returnObject(synthesizer) です。

    重要

    未完了または失敗したタスクから SpeechSynthesizer オブジェクトを返却しないでください。

  • タスク失敗時:SDK またはビジネスロジックからの例外によってタスクが中断された場合、以下の両方の操作を実行してください。

    1. 基盤となる WebSocket 接続を手動で閉じます。

    2. オブジェクトプール内のオブジェクトを無効化し、再利用を防止します。

    // 現在のコードでは、以下の対応となります:
    // 接続を閉じる
    synthesizer.getDuplexApi().close(1000, "bye");
    // プール内の不具合のある合成器を無効化する
    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
  • サービスが TaskFailed エラーを返した場合、追加のアクションは不要です。

ウォームアップおよびタイミングメトリック

DashScope Java SDK に対する同時呼び出しのレイテンシーなどのパフォーマンスメトリックを評価する際は、正式なテストの前にシステムをウォームアップしてください。ウォームアップにより、測定結果が安定状態におけるサービスの実際のパフォーマンスを正確に反映し、初期接続確立時間によるデータスキューを回避できます。

接続再利用メカニズム

DashScope Java SDK は、WebSocket 接続の管理および再利用を効率的に行うためのグローバルシングルトン接続プールを使用します。これにより、頻繁な接続確立および切断のオーバーヘッドが削減され、高い同時実行性のシナリオにおけるスループットが向上します。

このメカニズムの主な動作は以下のとおりです。

  • オンデマンド作成:SDK は起動時に WebSocket 接続を事前に作成しません。最初の呼び出し時にのみ接続を作成します。

  • 期間限定再利用:リクエストが完了した後、接続は最大 60 秒間プール内で再利用可能として保持されます。

    • 60 秒以内に新しいリクエストが到着した場合、既存の接続が再利用されます。これによりハンドシェイクのオーバーヘッドが回避されます。

    • 接続が 60 秒以上アイドル状態になると、リソース解放のために自動的に閉じられます。

ウォームアップの重要性

以下のケースでは、接続プールに再利用可能なアクティブな接続が含まれていない可能性があります。そのため、リクエストは新しい接続を作成する必要があります。

  • アプリケーションが起動直後であり、まだ呼び出しを行っていない場合。

  • サービスが 60 秒以上アイドル状態であったため、プール内のすべての接続がタイムアウトして閉じられた場合。

このような場合、最初または初期のリクエストは、TCP ハンドシェイク、TLS ネゴシエーション、プロトコルアップグレードを含む完全な WebSocket 接続確立プロセスをトリガーします。これらのリクエストのエンドツーエンドレイテンシーは、接続を再利用する後続のリクエストよりも大幅に高くなります。この余分な時間は、ネットワーク接続の初期化に由来するものであり、サービス処理によるものではありません。ウォームアップを行わないと、初期接続時間が含まれるため、パフォーマンステストの結果が歪んでしまいます。

ベストプラクティス

信頼性の高いパフォーマンスデータを収集するため、正式なストレステストまたはレイテンシー測定の前に、以下のウォームアップ手順を実行してください。

  1. 正式なテストと同じ同時実行レベルをシミュレートします。接続プールを十分に埋めるために、十分な数のウォームアップ呼び出し(例:1~2 分間)を送信します。

  2. 正式なパフォーマンスデータ収集を開始する前に、接続プールが十分な数のアクティブな接続を確立および維持していることを確認します。

適切なウォームアップにより、SDK の接続プールは安定した再利用状態に達します。これにより、より代表的なレイテンシーメトリックが得られ、実際のオンライン運用時のパフォーマンスを反映できます。

Java SDK の一般的なエラー

例外 1:サービスのトラフィックは安定しているが、サーバー上の TCP 接続数が継続的に増加する

原因:

タイプ 1:

各 SDK オブジェクトは作成時に接続を要求します。オブジェクトプールを使用しない場合、各タスク完了後にオブジェクトが破棄されます。この操作により、接続は参照されない状態となり、サーバー側の接続タイムアウト(61 秒)まで切断されません。その結果、この 61 秒間は接続を再利用できません。

高い同時実行性のシナリオでは、再利用可能な接続がない場合、新しいタスクが新しい接続を作成します。これにより、以下の問題が発生します。

  1. 接続数が継続的に増加します。

  2. 過剰な接続数がサーバーの利用可能なリソースを消費するため、サーバーのパフォーマンスが低下します。

  3. 接続プールが満杯になり、新しいタスクは利用可能な接続を待つためにブロックされます。

タイプ 2:

オブジェクトプールの MaxIdle パラメーターが MaxTotal パラメーター未満に設定されています。その結果、プールにアイドル状態のオブジェクトがある場合、MaxIdle 制限を超えるオブジェクトは破棄されます。このプロセスにより、接続リークが発生します。これらのリークした接続は、61 秒のタイムアウト後にのみ切断されます。タイプ 1 の原因と同様に、これにより接続数が継続的に増加します。

解決策:

タイプ 1 の原因に対しては、オブジェクトプールを使用してください。

タイプ 2 の原因に対しては、オブジェクトプールの構成パラメーターを確認し、MaxIdle および MaxTotal を同一の値に設定し、オブジェクトプールの自動破棄ポリシーを無効化してください。

例外 2:タスクの実行時間が通常の呼び出しよりも 60 秒長くなる

原因は 例外 1 と同じです。接続プールの最大接続数に達しており、新しいタスクは、参照されない接続が 61 秒タイムアウトするのを待ってから新しい接続を取得する必要があります。

例外 3:サービス起動直後はタスクが遅く、その後徐々に通常の速度に戻る

原因:

高い同時実行性の呼び出しでは、単一のオブジェクトが複数のタスクで WebSocket 接続を再利用します。したがって、WebSocket 接続は通常、サービス起動時にのみ作成されます。ただし、タスク起動段階で高い同時実行性の呼び出しを即座に開始すると、多数の WebSocket 接続を同時に作成することになり、ブロッキングが発生する可能性があります。

解決策:

同時実行数を徐々に増加させるか、サービス起動後にプリフェッチタスクを追加してください。

例外 4:サーバーが「Invalid action('run-task')! Please follow the protocol!」というエラーを報告する

原因:

クライアント側のエラーが発生した場合、サーバーには通知されず、接続はタスク進行中の状態のまま残ります。この接続および関連するオブジェクトが新しいタスクで再利用されると、プロトコルエラーが発生し、新しいタスクが失敗します。

解決策:

クライアント側の例外がスローされた後は、WebSocket 接続を明示的に閉じ、オブジェクトをオブジェクトプールへ返却する必要があります。

例外 5:サービスのトラフィックは安定しているが、呼び出しボリュームに異常なスパイクが発生する

原因:

多数の WebSocket 接続を同時に作成すると、ブロッキングが発生します。着信サービストラフィックは継続するため、短期的なタスクのバックログが発生します。ブロッキングが解消されると、すべてのバックログタスクが一度に呼び出されます。これにより、呼び出しボリュームのスパイクが発生し、一時的に Alibaba Cloud アカウントの同時実行制限を超えてしまう可能性があります。その結果、タスクの失敗、サーバーのパフォーマンス低下などが発生します。

多数の WebSocket 接続を同時に作成することは、以下のシナリオで典型的に発生します。

  • サービス起動段階

  • ネットワーク例外が発生し、多数の WebSocket 接続が同時に切断・再接続される場合

  • 多数のサーバー側エラーが同時に発生し、多数の WebSocket 再接続が発生する場合。一般的なエラーとして、「Requests rate limit exceeded, please try again later.(リクエストレート制限を超えました。しばらくしてから再度お試しください。)」など、同時実行数がアカウント制限を超えた場合があります。

解決策:

  1. ネットワーク状態を確認してください。

  2. スパイク発生前に多数の他のサーバー側エラーが発生していたかどうかを確認してください。

  3. Alibaba Cloud アカウントの同時実行制限を引き上げてください。

  4. オブジェクトプールおよび接続プールのサイズを縮小してください。また、オブジェクトプールの上限を用いて最大同時実行数を制限することもできます。

  5. サーバーの構成をスペックアップするか、サーバーの台数を増加させてください。

例外 6:同時実行数が増加するにつれてすべてのタスクが遅くなる

解決策:

  1. ネットワーク帯域幅制限に達していないか確認してください。

  2. 実際の同時実行数がサーバーの仕様に対して高すぎないか確認してください。