作者简介:玉修,携程技术专家,专注于电话音视频通信、智能客服机器人等领域。
一、前言
携程拥有庞大的呼叫中心,涉及上万客服人员,覆盖机票、酒店、火车票、度假等产线的售前售后业务,每天的电话业务量超百万通。近年来,通信技术、人工智能技术和智能终端等都在不断革新,我们也一直在思考如何去做更智能化、自动化的呼叫中心,为未来海量的客户需求提供稳定和优质的服务。
携程呼叫中心的智能化包含多个方面:
-
用户侧:智能在线聊天机器人(IM)、智能语音导航/智能语音客服机器人/智能邀评插件(电话)
-
客服侧:智能工单和排班系统、智能质检系统、智能客户资源管理系统、服务渠道智能化
-
系统基建:平台部署智能化、业务监控智能化
本文旨在探讨携程实现呼叫中心电话智能语音客服机器人的基建服务------语音识别服务(即ASR)的负责均衡的演进历程,以及最佳实践。
二、背景
随着人工智能技术的发展,在呼叫中心业务中,传统的 IVR(交互式语音应答)按键导航模式逐步向IVR智能客服机器人转变(客户与IVR机器人进行语音对话的方式来办理业务)。携程呼叫中心系统下的IVR业务也在不断地向电话智能语音机器人转变,目前携程酒店、机票、火车票的国内IVR呼入业务,以及IBU国际英语机票的IVR呼入业务,已经全部由电话智能语音机器人来为客户提供自助服务。
下图是国内酒店业务场景中,客户拨打携程服务热线后,客户与电话机器人通过语音沟通的记录。可以看出,客户顺利完成了"取消订单"的业务办理。
智能客服机器人要想实现上图的交互效果,离不开ASR服务的使用,以及功能完善且稳定的呼叫中心系统的支撑。携程呼叫中心的整个平台依赖了众多组件,底层包括CC-Gateway(语音网关)、SBC(会话边际控制服务)、REG(分机注册服务)、SM(会话管理服务)、RS(呼叫路由服务)、CM(呼叫管理服务,基于FreeSWITCH)、ASR(语音识别服务)等系列服务。
下图是携程呼叫中心,客户呼入到智能客服机器人场景,进行语音自助业务办理时所涉及的部分核心组件架构图。
从上图可以看出,携程呼叫中心系统底层(如FreeSWITCH)调用实时ASR完成语音识别是基于MRCP协议来实现的。我们将上图中涉及ASR使用部分的组件交互进行简化,得出其包含下面3种组件:
-
MRCP客户端:发送RTP和SIP/MRCP的发起者,如FreeSWITCH(下文简称FS
-
MRCP服务端:处理MRCP/SIP信令,接收并转发RTP
-
ASR引擎 :解析RTP,将语音转换成文本,并返回给MRCP Server
可以发现,对于呼叫中心ASR调用者而言,只需要关心怎么对接MRCP Server即可,无需关注ASR Engine部分。在实际使用过程中,如果你采购第三方ASR系统进行私有化部署的话(比如科大讯飞ASR、百度ASR),通常MRCP Server和ASR Engine是打包在一起,并部署在同一机器上。
但无论你采购哪家的ASR产品进行集群化部署,厂商都没有提供ASR的负载均衡解决方案,需要客户自行解决。携程为了让ASR引擎具备更高的可用性,采用了多集群、多IDC、多供应商的ASR产品(如携程自研、百度、阿里、微软等)来提供服务。针对这么多的集群和ASR产品,设计出一个调度策略和负载均衡方案来合理有效地利用ASR资源就变得极为重要了。
三、方案探索
目标已经理清,接下来深入分析调用ASR涉及的技术点,看看如何实现目标。
调用MRCP Server包含SIP(UDP/TCP)、MRCP(TCP)、RTP(UDP)三部分,MRCP和RTP的服务端地址是由SIP INVITE的响应 200 OK中SDP指定(如下图),所以只要完成对SIP的负载均衡就能解决另外两个,要给MRCP Server做负载均衡就变成了给 SIP(UDP/TCP)做负载均衡。
我们期望负载均衡的效果是:只要MRCP-Server服务端集群下有多台机器,即使客户端只有一个,负载均衡设备也能将请求均匀分发给服务端的每一个成员。
常规的负载均衡方案,无外乎基于硬件负载均衡设备实现,如A10(即AX)、F5、NetScaler等;或者基于软负载实现,如LVS、Nginx等。但这些常规方法,都无法真正做到给MRCP Server实现负载均衡。
携程的基建服务中,恰好有AX、Netscaler、TDLB相关负载均衡服务,所以我们基于这几种基建服务都进行过验证性测试,可惜最终效果都不尽人意。
以FS作为MRCP Client,AX作为负载均衡设备为例。假如只有1台FS设备,1台AX设备,4台MRCP-Server设备。从FS依次发起4次请求,或者同时发起4次请求,最终使ASR驻留并发达到4个。
上图是左侧"卖家秀"是我们想要达到的预期效果,右侧"买家秀"是我们实验所得的实际效果,所有的请求都被分配到了同一台MRCP-Server机器上,没能均匀的分配给集群下的各成员。理想很丰满,但现实太骨感。
那么,AX设备没能做到均匀分配的原因是什么呢?FS基于AX来给MRCP做负载又存在什么弊端呢?
首先,对于FS和AX设备相对固定的情况下,SIP请求的IP四元组(Source IP、Source port、Destination IP、Destination port)不会发生变化,因为FS对接MRCP Server时,会在MRCP配置文件中指定客户端和服务端的IP/Port,所以AX每次分配给FS的MRCP Server都是同一台,这显然不符合负载均衡的预期。
其次,电话场景,在收到200 OK后,可能长达半小时不会再有SIP交互,期间的MRCP和RTP都是MRCP-Client和MRCP-Server之间进行直连交互,根本不经过AX设备,而AX设备默认的会话保持时长为120秒,超过这个时间,SIP通道会被AX关闭,这会导致后续的SIP无法送达。
既然此路不通,我们要考虑其他解决方案,经过深入研究和各种尝试,认为下面这两种解决方案比较适合,但各有优缺点:
-
方案A:通过FreeSWITCH的distributor模块实现
-
方案B:通过OpenSIPs实现
|-----|---------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| | 优点 | 缺点 |
| 方案A | 1、无需依赖第三方负载均衡组件 | 1、配置繁琐复杂 2、MRCP Server节点增删,都需要调整FS配置文件,而且得在无ASR业务时,才能加载生效 3、端口数量消耗大(每个MRCP Server都需要单独分配端口段) 4、负载均衡策略相对单一,只支持按比例分配。而且单机所占有的最小比例不能小于0 |
| 方案B | 1、配置简单 2、MRCP Server节点增删,只需调整OpenSIPs的DB即可,有ASR调用时,也可更改,实时生效 3、端口数量消耗小(只需要配置一个MRCP Profile文件,多个MRCP Server共用端口段) 4、负载均衡方案多种多样,支持按比例、轮询等多种方式 | 1、需要依赖第三方负载均衡组件OpenSIPs |
我们最终将两个方案结合,来实现负载均衡,FS上使用distributor模块来实现对 OpenSIPs做负载均衡,OpenSIPs上再对MRCP-Server做负载均衡,效果如下:
1)FS、OpenSIPs、MRCP-Server三个组件之间实现了IDC优先就近访问(如上所述,FS未能做到100%的就近访问)。
2)当相同IDC下的下游服务全部不可用时,则自动将流量分配到其他IDC下,如下图,IDC-A 下 FS的ASR请求,优先请求到 IDC-A 的OpenSIPs,然后IDC-A的OpenSIPs再根据分配策略,将请求优先分配给 IDC-A下的MRCP-Server,如果IDC-A下的MRCP-Server全宕机了,会自动分配给IDC-B下的MRCP-Server。
3)负载均衡服务可自动检测下游集群各成员的状态,当某成员服务不可用时自动拉出,服务状态恢复后,再自动拉入。FS和OpenSIPs都是通过发送SIP OPTION 来自动探测下游服务的状态。
四、方案实践
接下来,我们详细看看每种方案的具体实现方式。以下方案运行环境为:CentOS 7.6、FreeSWITCH 1.6.20、OpenSIPs 2.4.2。
本篇文章中,我们不详细讲解每种方式的实现原理,只介绍解决方法,有兴趣的同学可以自行学习FS和OpenSIPs的相关功能点,这里给出几个链接:
假设我们只有一台FS作为MRCP 客户端,并且MRCP Server 集群中有两台服务器,分别是 mrcp1 和 mrcp2,希望FS针对每一通电话执行ASR命令时,请求可均匀分配给两个MRCP Server。
4.1 基于FS的distributor模块实现MRCP Server的LB
该方案的核心思路如下:
1)FS直接与MRCP Server对接,为MRCP Server集群下每一个成员配置一个profile
2)将MRCP Server集群下的所有成员配置成 FS 的网关,并开启网关的SIP OPTION探测功能,同时确保gateway的name要与mrcp_profile文件中profile的name一致
3)通过FS的distributor 模块为这些MRCP网关配置负载均衡策略
4)最后,实际执行ASR命令时,先通过 expand eval {distributor mrcp {sofia profile external gwlist down}} 负载均衡分配得到一个可用的 MRCP Server Profile的名称,然后用该MRCP Profile的名称作为FS play_and_detect_speech ASR命令的参数即可。
详细的配置步骤如下所示:
第一步:FS与MRCP Server对接
bash
<span>在 /usr/local/freeswitch/conf/mrcp_profiles/下配置FS对接MRCP Server的文件</span>
下面只给出mrcp1.xml的部分核心配置,只需要确保mrcp1.xml和mrcp2.xml里client-port、rtp-port-min、rtp-port-max配置不同即可。
这里可以看到,如果MRCP Server集群有很多机器,那么这里的RTP端口段可能不够用,一通电话进行ASR解析需要2个端口,一个用来传输RTP、一个传RTCP。
xml
<span><include></span>
第二步:配置FS网关
bash
<span>在 /usr/local/freeswitch/conf/sip_profiles/external/下配置网关对接文件</span>
mrcp1.xml 的详细配置如下:
xml
<span><gateway name="mrcp1"> 【gateway的name要与mrcp_profile文件中profile的name一致,或可以按照某种规则转换】</span>
第三步:配置FS的distributor模块
bash
<span>vim /usr/local/freeswitch/conf/autoload_configs/distributor.conf.xml</span>
最后,来看看使用效果。按照上述配置,将mrcp2服务宕机后,执行负载均衡的效果如下:
css
<span>freeswitch@LPT0596> sofia profile external gwlist down 【获取宕机的网关】</span>
其他FS相关命令:
-
reload mod_unimrcp : 修改FS与MRCP server对接的文件后,重新加载生效【只有当前没有正在执行的ASR操作时,才能重加载】
-
sofia profile external rescan : 重新加载FS的网关配置
4.2 基于OpenSIPs实现MRCP Server的LB
4.2.1 核心思路
FS不直接与MRCP Server对接,而是与OpenSIPs进行对接。对接方式是把OpenSIPs配置成一个MRCP profile,文件中的server-ip 和 server-port 地址配置成OpenSIPS 的服务地址即可。
FS执行ASR命令时,先将SIP请求发送给OpenSIPs,再由OpenSIPs负载均衡到MRCP Server集群中的成员,交互的时序图如下:
4.2.2 方案分析
通过OpenSIPs来实现对MRCP的负载均衡需要解决下面几个问题:
问题1、如何判断收到的INVITE请求是要执行ASR命令,还是普通呼叫命令?
问题2、知道是执行ASR命令后,如何选择MRCP Server,进行分配?
问题3、如果有多套MRCP Server集群,比如一套百度MRCP,一套阿里MRCP,客户端希望能指定引擎使用,该如何解决?
既然已经明确了问题点,那咱就各个击破即可,下面是各问题点的解决方法:
- 问题1的解决方法
我们来看一条FS发送给OpenSIPs,请求执行MRCP负载均衡的SIP INVITE信息,其中 192.168.1.99是FS,192.168.1.18是OpenSIPs。
less
<section><code><span>INVITE sip:192.168.1.18:5070 SIP/2.0</span></code><code><span>Via: SIP/2.0/UDP 192.168.1.99:5102;rport;branch=z9hG4bKQ21yZS46ytrgF</span></code><code><span>Max-Forwards: 70</span></code><code><span>From: <sip:192.168.1.99:5102>;tag=4B8SvQe66FNvc</span></code><code><span>To: <sip:192.168.1.18:5070></span></code><code><span>Call-ID: ed9f5f6b-0673-123b-199a-fa163e72d95e</span></code><code><span>CSeq: 47770741 INVITE</span></code><code><span>Contact: <sip:192.168.1.99:5102></span></code><code><span>User-Agent: FreeSWITCH</span></code><code><span>Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE</span></code><code><span>Supported: timer, 100rel</span></code><code><span>Content-Type: application/sdp</span></code><code><span>Content-Disposition: session</span></code><code><span>Content-Length: 306</span></code><code><span><br></span></code><code><span>v=0</span></code><code><span>o=FreeSWITCH 2480643166757753319 6144298267054033408 IN IP4 192.168.1.99</span></code><code><span>s=-</span></code><code><span>c=IN IP4 192.168.1.99</span></code><code><span>t=0 0</span></code><code><span>m=application 9 TCP/MRCPv2 1</span></code><code><span>a=setup:active</span></code><code><span>a=connection:existing</span></code><code><span>a=resource:speechrecog</span></code><code><span>a=cmid:1</span></code><code><span>m=audio 31799 RTP/AVP 0 8</span></code><code><span>a=rtpmap:0 PCMU/8000</span></code><code><span>a=rtpmap:8 PCMA/8000</span></code><code><span>a=sendonly</span></code><code><span>a=mid:1</span></code></section>
从上面信令可以看到,FS发起的INVITE中,没有主被叫号码信息,只有FS和OpenSIPs的IP和端口信息。如果我们的OpenSIPs只用来给MRCP Server做负载均衡,那么就很简单,收到INVITE请求,都认为是请求执行ASR命令,分配给MRCP Server即可。但是,OpenSIPs只给MRCP Server做负载岂不是大材小用了!
所以,实际我们不会这样使用,OpenSIPs通常还会给其他呼叫中心组件做负载均衡,比如给FreeSWITCH、语音网关、分机注册服务等做LB。这样OpenSIPs就会收到来自各种组件的SIP INVITE请求。那么该如何判断收到的 INVITE 是要执行ASR命令,还是要做其他业务呢?
常规思路,自然是OpenSIPs分析INVITE的SIP消息头,从中进行判断。可是由于FS的mod_unimrcp模块的限制,FS执行ASR命令时,发送的SIP INVITE里不支持增加自定义SIP消息头,所以只能从标准 SIP 消息头中进行挖掘。
-
根据INVITE请求的源IP:不可行,因为同一个源IP可能发起多种请求的INVITE,比如FS可能是请求执行ASR,也可能是请求呼叫手机;此外,即使可行,源IP也不方便维护。
-
根据INVITE请求的目的IP:不可行,所有INVITE请求的该值都一样
-
根据INVITE请求的User-Agent头:可行,OpenSIPs通过$ua就能获取该值。虽然不能针对每次INVITE自定义不同的UA头,但FS对接MRCP Server的Profile中可以指定一个统一的User-Agent头,默认是FreeSWITCH。
-
根据INVITE请求SDP信息中的'm'头:可行,OpenSIPs通过$(rb{sdp.line,m})就能获取该值。如 上面报文中"m=application 9 TCP/MRCPv2 1" 里面有MRCPv2,可根据这个判断是执行ASR。
建议使用User-Agent头进行区分,取值方便,效率高。所以,FS对接OpenSIPs时,配置的MRCP Profile时,指定一个特别的User-Agent,比如叫ASR_MRCP_CLIENT_FS,OpenSIPs收到INVITE请求,优先判断UA信息,如果是ASR_MRCP_CLIENT_FS,那么就是要执行ASR命令。
- 问题2的解决方法
可以使用OpenSIPS的load_balancer 或 dispatcher 模块来实现对 MRCP Server 服务端的负载均衡,两种方式的特点如下:
如果MRCP-Server集群下的成员可支持的并发数不一样,想做到哪台机器剩余的可用资源最多,就优先分配给谁,当各成员可用资源数相同时,在轮训分配,那么可以使用 load_balancer 模块来实现负载均衡;
如果MRCP-Server集群下的成员可支持的并发数完全一样,无差别,那么建议使用dispatcher模块来试想负载均衡,可以做到均匀的将请求分配给每一台服务器。
|---------------|---------------------------------------------------|------------------------------------------------------------------------------------------------|
| | 优点 | 缺点 |
| load_balancer | 可控制每个MRCP Server的最大并发量 支持监控分配给每个MRCP Server的实时并发量 | 分配策略单一:只支持空闲优先策略分配和按比例分配两种策略,无法支持记忆轮训,这就导致但MRCP Server集群新增成员时,会将流量全部分配给新增的机器,这种情况,新机器的突增压力可能较大 |
| dispatcher | 分配策略多种多样:如支持记忆轮训、Hash分配等 | 不能控制每个MRCP Server的最大并发量,话务量暴涨时,存在雪崩隐患 不能监控分配给每个MRCP Server的实时并发量(但可以自行通过OpenSIPs其他模块实现) |
- 问题3的解决方法
在FS上为每一套MRCP Server集群,配置一个MRCP Profile并且都指向OpenSIPs,但User-Agent的值配置成不一样,OpenSIPs根据UA的不同,来选择该给哪个集群做LB。
css
<span>baidu_mrcp_lb.xml 下面只给出特有配置,其他配置被省略了</span>
OpenSIPs给MRCP Server做负载均衡的处理流程图如下:依赖dialplan模块进行选择具体通过哪个模块来执行LB。
4.2.3 具体实现
如果OpenSIPs本身也是集群化部署,那么可以通过本文3.1章节的方法实现对OpenSIPs的负载均衡。
下面代码涉及OpenSIPs对dialplan、dispatcher、load_balancer几个模块的使用,本文不讲解这部分的使用方法。
- 数据库初始化
说明:
1)下方配置了百度、阿里两个MRCP Server集群,并且每个集群都部署在了两个IDC(IDC_A和IDC_B)
2)OpenSIPs根据dialplan拨号方案来为阿里和百度选择负载均衡的方式,dialplan表中字段"attrs"配置逻辑是:[MRCP集群第一路由的集群ID:负载均衡实现方式:集群名称],如"90:DS:ASR_MRCP_SERVER_CTRIP_ALI"代表,阿里MRCP第一路由的集群ID是90,采用dispacher模块实现LB;"90:DS:ASR_MRCP_SERVER_CTRIP_ALI"代表,百度MRCP第一路由的集群ID是91,采用load_balancer模块实现LB
3)无论是dispacher,还是load_balancer,都配置了单IDC下负载均衡的基础上,增加了逃生路由的功能。集群ID为 90/91代表第一路由,10090/10091代表第二路由
css
<span>dialplan的attrs字段被赋予了特殊用途</span>
配置好后,可查看集群内MRCP成员的状态:
bash
<span>sudo /usr/local/opensips/sbin/opensipsctl fifo lb_list</span>
- OpenSIPs代码实现
css
<span>route{</span>
如果按照上面脚本执行了 <math xmlns="http://www.w3.org/1998/Math/MathML"> r u = " s i p : " + ru = "sip:" + </math>ru="sip:"+rU + "@" + <math xmlns="http://www.w3.org/1998/Math/MathML"> ( d u u r i . h o s t ) + " : " + (du{uri.host}) + ":" + </math>(duuri.host)+":"+dp;,但是 <math xmlns="http://www.w3.org/1998/Math/MathML"> r U = = n u l l 并且不设置 rU== null 并且不设置 </math>rU==null并且不设置rU="Null2SM"或者其他非空值,会报如下错误:
perl
<span>Feb 12 22:27:35 fat5410 /usr/local/opensips/sbin/opensips[3710]: ERROR:core:parse_uri: bad char '@' in state 0 parsed: <sip:> (4) / <sip:@192.168.1.190:8060> (20)</span>
解决办法:
1)设置$rU 为一个非空值
2)直接不修改$ru 的值
3)修改 <math xmlns="http://www.w3.org/1998/Math/MathML"> r u = " s i p : " + ru = "sip:" + </math>ru="sip:"+(du{uri.host}) + ":" + $dp;
4.2.4 信令记录:
- FS 发送INVITE给 OpenSIPs
xml
<span>2022-02-13 13:50:53 +0800 : 192.168.1.99:5221 -> 192.168.1.18:5070</span>
- OpenSIPS 转发INVITE给 MRCP server
xml
<span>2022-02-13 13:50:53 +0800 : 192.168.1.18:5070 -> 192.168.1.190:8060</span>
- MRCP Server 回复200 OK,返回后续接收RTP的真实地址
xml
<span>2022-02-13 13:50:53 +0800 : 192.168.1.190:8060 -> 192.168.1.18:5070</span>
- FS发送ACK给OpenSIPs
xml
<span>2022-02-13 13:50:53 +0800 : 192.168.1.99:5221 -> 192.168.1.18:5070</span>
- 最后,OpenSIPs将ACK转发给MRCP Server
五、结语
从上文中提到的携程呼叫中心客户呼入到智能客服机器人场景的核心组件架构图可以看出,ASR引擎的负载均衡只是携程呼叫中心平台各组件中很小的一个功能点,但也是不可或缺的一部分。
正因为有了这个技术方案的实现,使得多集群、多数据中心、多供应商的ASR产品得以很好地整合,为携程电话智能客服机器人业务的稳定运行提供了良好的技术保障,提升了携程客户的通话体验。
本文主要讲解了对于ASR引擎做负载均衡的设计以及实现方案,希望能对从事智能呼叫中心领域工作或研究的同学们提供一些帮助。