【最佳实践系列】高并发调用百炼语音合成大模型

简介: 本文介绍了阿里云百炼的CosyVoice语音合成大模型及其高并发调用优化方案。CosyVoice支持文本到语音的实时流式合成,适用于智能设备播报、音视频创作等多种场景。为了高效稳定地调用服务,文章详细讲解了WebSocket连接复用、连接池和对象池等优化技术,并通过对比实验展示了优化效果。优化后,机器负载降低,任务耗时减少,网络负载更优。同时,文章还提供了异常处理方法及常见问题解决方案,帮助开发者更好地集成和使用SDK。

一、引言

CosyVoice是通义实验室依托大规模预训练语言模型,深度融合文本理解和语音生成的新一代生成式语音合成大模型,支持文本至语音的实时流式合成。可以应用于:

  • 智能设备/机器人播报的语音内容,如智能客服机器人、智能音箱、数字人、语音助手等。
  • 音视频创作中需要将文字转为语音播报的场景,如小说阅读、新闻播报、影视解说、剧本配音等。


随着开发者开始接入阿里云百炼的语音合成、语音识别等服务,如何高效、稳定地通过 java SDK 调用百炼的语音服务变得至关重要。目前阿里云百炼上已经上线的最新语音模型包括:cosyvoice-v1(语音合成)sambert (语音合成)paraformer-realtime-v2(语音识别)paraformer-v2(离线文件转写)sensevoice-v1(离线文件转写),其中前两个模型为实时调用,使用 websocket 协议,后两个模型为异步调用,使用 https 协议。由于离线任务不具备高实时性要求,因此不在本文的探讨范围。

本文会以 cosyvoice 大模型语音合成服务为例,详细介绍如何通过 SDK 高并发调用百炼语音服务。请在使用时一定仔细阅读《异常处理》章节,避免引入新的错误。

⚠️注意!

在 dashscope-sdk-java 版本大于等于 2.16.9,才支持针对高并发场景的优化,因此请使用最新版本的 SDK 进行集成。

二、一些基础知识

什么是 Websocket 连接

WebSocket 是一种用于在客户端(比如浏览器)和服务器之间建立持久性双向通信的协议。简单来说,它允许客户端和 ws 服务器和服务器之间保持一个打开的连接,从而实现实时数据传输。

WebSocket的特点:

  1. 双向通信:与传统的HTTP请求-响应模式不同的是,WebSocket允许服务器主动向客户端发送数据,反之亦然。这使得实时应用(如聊天应用、在线游戏等)更加高效。
  2. 持续连接:建立WebSocket连接后,双方可以通过同一个连接进行多次交互,而不需要每次都重新建立连接。这减少了连接建立的开销,提高了性能。
  3. 低延迟:由于是持久连接,WebSocket可以迅速发送和接收消息,这对于需要快速反应的应用非常重要。
  4. 支持文本和二进制数据:WebSocket可以传输文本数据(如JSON、HTML等)和二进制数据(如图像、音频等),适用范围广泛。


WebSocket 双向通信、持续连接的特点非常适合实时上传音频,接收识别结果的实时语音识别和实时上传文本,接收音频片段的语音合成。因此在实时语音服务中,使用 websocket 作为服务协议。

WebSocket的工作流程:

  1. 建立连接:客户端通过HTTP请求向服务器发起一个WebSocket连接。当服务器接受请求后,会返回一个特殊的响应,表示双方成功建立了WebSocket连接。
  2. 数据传输:一旦连接建立,客户端和服务器可以随时发送和接收数据,而不需要重新建立连接。
  3. 断开连接:当需要关闭连接时,任意一方都可以发送一个关闭请求,连接就会被优雅地关闭。


基于安全考虑,使用了 WebSocket Secure(简称 wss)连接,在 WebSocket 连接基础上增加了 SSL/TLS 加密层,确保传输的安全性。(注:在下文中,将不区分 wss 连接和 ws 连接)。

WebSocket 的缺点:

  1. 虽然 WebSocket 连接可以在建立连接之后显著的降低网络延迟,但是在创建连接时却需要较久的时间。这是由于在建立连接时,首先需要进行三次握手建立 tcp 连接以及一次升级协议握手。此外证书验证也会占用额外的时间。
  2. WebSocket保持持久连接,这意味着服务器端的资源会被占用。对于高并发的应用,服务器可能需要更多的内存和处理资源,管理大量的连接可能会增加服务器的负担。

三、高并发场景下的优化方案

1.  连接复用

简单的讲:

由于 WebSocket 连接具有持续连接的特点,为了避免重复创建连接的开销,百炼网关支持将同一个 WebSocket 连接复用在多个任务中。

具体点:

百炼网关为每一个连接分配一个 conn_id,并为每一个基于 WebSocket 连接的任务分配一个 task_id。一个 conn_id 可以对应多个 task_id。在每一个任务中,百炼规定了 "run-task" 到 "task-finished" 的任务生命周期,在一个任务结束后,可以立刻开始下一个任务的生命周期。

 image.png



有了连接复用后,可以在同一个 websocket 连接中连续进行多次的实时语音任务。

举个例子:

在下图的日志中,同一个 WebSocket 连接(相同 conn_id)在半小时内复用了 1160 次,执行了 1160 次 cosyvoice-v1 的语音合成任务。

 image.png

2.  连接池

简单的讲:

连接池机制是一种管理 WebSocket 连接的方式。连接池是一个固定容积的池子,每次需要创建新的 WsbSocket 连接时,直接从中取出一个就行,在用完后再放回去。

具体点:

在百炼 java SDK 中使用了 okhttp3 提供的连接池。连接池可以替使用者管理连接的存放和取用、回收以及资源限制。下面详细介绍连接池的特性。

  1. 连接的建立与释放:
  • 连接池初始时为空。
  • 当客户端需要与服务器建立 WebSocket 连接时,可以从连接池中获取一个可用的连接。如果池中没有可用连接,则会创建一个新的连接并返回给客户端。
  • 当不再需要该连接时,它不会被立即关闭,而是返回到连接池中,以备后续使用,从而节省了连接的创建和销毁开销。
  1. 线程安全:
  • 在高并发的应用中,多个线程可能会同时请求连接,因此连接池需要是线程安全的,以避免状态不一致和数据竞争问题。
  1. 资源管理
  • 此外,连接池会管理连接的最大数量,包括总连接数和单个 IP 最大连接数。防止连接过多造成资源的浪费。
  • 当取用连接数大于连接池容积时,会阻塞等待直到有其他任务释放连接。

使用方法:

在 java SDK 中已集成了连接池,不需要额外代码。可以通过三个环境变量配置 java SDK 中连接池的大小:

DASHSCOPE_CONNECTION_POOL_SIZE

配置连接池大小。

推荐配置为您的峰值并发数的2倍以上。默认值为32。

说明

对象池大小需要小于等于连接池大小,不然会出现对象等待连接的情况造成调用阻塞。

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

配置最大异步请求数。

推荐配置为和连接池大小一致。默认值为32。

更多信息参见参考文档

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

配置单host最大异步请求数。

推荐配置为和连接池大小一致。默认值为32。

更多信息参见参考文档



3.  对象复用和对象池

简单的讲:

在面向对象的编程语言中,对象对应着资源,对象的创建和销毁对应着资源的申请和释放。在高并发的场景中,会频繁的创建和销毁 SDK 对象以及申请、释放对应的连接。对象池和连接池类似,是一个用来管理 SDK 对象的固定容积的池子,每次需要创建新任务时,直接从中取出一个,在用完后再放回去。

具体点:

百炼所有的语音语音 SDK 类都支持对象复用,可以重复的使用同一个对象创建无数次任务。由于 SDK 内部没有集成对象池,因此我们使用apache.commons.pool2 提供的对象池管理 SDK 对象。

  1. 对象的创建与复用:
  • 连接池初始时为空。
  • 当应用需要一个对象时,它会从对象池中获取一个已有的对象。如果对象池中没有可用的对象,它可能会创建一个新的对象。
  • 使用完对象后,应用并不销毁它,而是将其返回到对象池中,以便下次使用。
  1. 线程安全:
  • 在高并发的应用中,多个线程可能会同时请求对象,因此对象池需要是线程安全的,以避免状态不一致和数据竞争问题。
  1. 资源管理:
  • 对象池需要管理池的大小,包括最大和最小对象数量,以避免资源的过度消耗。
  • 当取用对象数大于对象池容积时,会阻塞等待直到有其他任务释放对象。

使用方法:

cosyvoice-v1 对应的audio.ttsv2.SpeechSynthesizer 对象为例,介绍如何创建和使用对象池。

步骤一:配置三方依赖

以 maven 为例,通过在 pom.xml 中添加下述对象池依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>the-latest-version</version>
</dependency>

步骤二:创建全局对象池

package com.alibaba.example;

import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
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.util.concurrent.locks.Lock;

class SpeechSynthesizerObjectFactory
        extends BasePooledObjectFactory<SpeechSynthesizer> {
    private String dashScopeApiKey;
    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("Using Object Pool Size In Env: "+ DEFAULT_OBJECT_POOL_SIZE);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("Using Default Object Pool Size: "+ 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;
    }
}

请参考上面的代码创建你的对象池,它可以放在项目中一个独立的 java 文件中管理。

在上面的代码中,创建了一个对象池的封装类:CosyvoiceObjectPool。它具有如下特点:

  1. 单例。每一个服务器上只需要一个对象池管理所有的对象。
  2. 通过静态方法 getInstance 以线程安全的方式获取对象池并借用或返还对象。
  3. 通过环境变量“COSYVOICE_OBJECTPOOL_SIZE”在服务启动前配置对象池大小,避免启动顺序带来的配置失效。

步骤三:使用对象池

将之前使用创建对象的方式调用语音服务的代码替换成从对象池中获取对象,记得要在最后将对象归还给对象池。

这里以最简单的同步调用语音合成为例:

public void run() {
    SpeechSynthesizer synthesizer = null;
    long startTime = System.currentTimeMillis();

    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("收到音频,音频文件流length为:"
                            + 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();
            }
        }

        // 将your-dashscope-api-key替换成您自己的API-KEY
        String dashScopeApiKey = "your-dashscope-api-key";

        SpeechSynthesisParam param =
            SpeechSynthesisParam.builder()
                .model("cosyvoice-v1")
                .voice("longxiaochun")
                .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("Exception e: " + e.toString());
            synthesizer.getDuplexApi().close(1000, "bye");
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        if (synthesizer != null) {
            try {
                // Return the SpeechSynthesizer object to the pool
                CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    long endTime = System.currentTimeMillis();
    timeCost = endTime - startTime;
    System.out.println("[线程 " + Thread.currentThread()
        + "] 语音合成任务结束。耗时 " + timeCost + " ms, RequestId "
        + requestId);
}

关键代码:

  1. 第 45 行代码:从对象池中获取 SpeechSynthesizer 对象。
  2. 第 46 行代码:重制 SpeechSynthesizer 对象,并更新用于语音合成的配置和回调。
  3. 第 55 行代码:在出现异常时,需要中断出现错误的 websocket 连接后再归还对象。
  4. 第 63 行代码:在对象 SpeechSynthesizer 使用完成后归还到对象池中,供其他任务复用。

⚠️注意!

每一个对象会在创建时从连接池中获取连接,并在销毁时归还连接。因此对象的生命周期和连接的生命周期并不一致,连接的生命周期会更长。

四、对比实验

通过使用优化和不使用优化对比实验验证上述优化方案的效果。实验配置如下:

  • 服务器规格:x86 8269CY(Cascade Lake)4 核心 8G 内存
  • SDK 版本:2.16.9
  • 测试策略:固定线程数为 100,每一个线程循环执行 Cosyvoice 语音合成任务,合成固定文本。

优化版本使用了前文中对象池和连接池调用,无优化版本不使用对象池、并且会在每次任务结束后手动关闭 WebSocket 连接。

监测工具

  • 通过 top 工具监测 CPU 利用率
  • 通过 sysstat 工具,sar 指令监测网络上/下行数据
  • 通过 ss -nt | wc 命令统计当前 TCP 连接数(WebSocket 连接和 TCP 连接一一对应)

测试结果:



无优化

优化

CPU

33%

20%

任务最大耗时

12 秒

5秒

任务平均耗时

5587ms

3974ms

网络负载(下行)

7M

4M

5分钟完成任务数

5433

7602

测试结论:

经过优化后具有如下优势:

  1. 机器负载降低,相同机器规格可以支持更高的并发数。实际测量单机最大并发数可以从 50 提升至 100。
  2. 任务耗时降低。避免了创建 WebSocket 连接和 SDK 对象的耗时。
  3. 由于避免了反复创建连接,因此具有更低的网络负载。
  • 由于优化后单位时间完成任务数更多,因此实际有效网络负载更高。这说明大量的网络带宽在优化前被用于反复握手、建连和丢包后重发。
  1. 避免服务启动后,瞬时创建过多 WebSocket 造成的卡顿。
  • 无优化版本的最大耗时显著高于平均耗时,这是由于测试中途瞬时创建过多 WebSocket 造成的卡顿。

五、异常处理

在复用对象和连接优化方案后,在带来性能提升的同时也会带来更多的异常情况。百炼语音任务的异常包括服务端报错和客户端报错,接下来会分别介绍两种报错的底层原理和异常处理。

服务端报错:

服务端常见报错:常见报错包括 InvalidParameter,Internal Error 等。分别是由于客户端的错误调用或服务端出现自身问题(如资源不足)引发。


服务端报错处理:服务端报错后会下发 TaskFailed 消息,并立刻主动中断 WebSocket 连接。此时得益于 SDK 内部的自动重连机制,用户不需要额外处理这一类的异常,SDK 会在下一次复用这个已关闭连接时自动重连。


客户端报错:

客户端常见报错:非法输入、非法调用、调用中途客户业务代码抛出异常等。由于客户端报错和业务代码密切相关,SDK 内部无法处理所有的异常情况。


客户端报错处理:需要客户端主动捕获可能出现的异常,并且在此时关闭 WebSocket 连接后再将对象归还到对象池中。

主动关闭连接方法如下:

// cosyvoice-v1
synthesizer.getDuplexApi().close(1000, "bye");
// paraformer-realtime-v2
recognizer.getDuplexApi().close(1000, "bye");
// sambert-xxx-v1
synthesizer.getSyncApi().close(1000, "bye");

常见异常状况:

由于对象池和连接池引发的常见异常情况及解决方案请参考百炼文档

更多调用SDK时遇到的异常请参考github项目


👉阿里云百炼详情了解可点击此官网链接:阿里云百炼官网介绍

👉阿里云百炼控制台页面可点击此链接直接进入阿里云百炼控制台


六、联系方式

我们在 GitHub 分享了百炼语音 SDK 示例代码。提供了丰富的示例,涵盖语音识别、语音合成、语音对话等多种功能。通过这些示例,您可以快速学习如何使用 SDK,并将其集成到您的应用程序中,打造出更智能、更人性化的用户体验。


如果您在调用百炼语音服务时遇到任何问题,或是有更多的开发建议或需求,欢迎通过钉钉群联系:

 image.png


相关文章
|
6天前
|
调度 云计算 芯片
云超算技术跃进,阿里云牵头制定我国首个云超算国家标准
近日,由阿里云联合中国电子技术标准化研究院主导制定的首个云超算国家标准已完成报批,不久后将正式批准发布。标准规定了云超算服务涉及的云计算基础资源、资源管理、运行和调度等方面的技术要求,为云超算服务产品的设计、实现、应用和选型提供指导,为云超算在HPC应用和用户的大范围采用奠定了基础。
179577 18
|
13天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
15天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
9292 23
|
19天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
4958 12
资料合集|Flink Forward Asia 2024 上海站
|
19天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
27天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
15天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
|
14天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
1125 71