貝殼音頻流網關的設計、演進及使用場景
介紹
隨着實時音視頻技術的發展,音視頻聊天已經走入尋常百姓家,微信 2017 年每天有 4.1 億次的音視頻通話。貝殼找房 IM 作爲線上商機來源,也實現了該功能,作爲經紀人和客戶溝通的便捷工具。除了 IM 音視頻通話,實時音視頻技術在貝殼還用到了直播自動審覈、經紀人講盤訓練等各種場景,這些場景中都用到了實時音頻流網關。以經紀人講盤訓練爲例,通過構造一個虛擬人進入音頻房間,可以和經紀人進行實時問答,模擬買房和講盤,提高經紀人作業效率。要達到該效果,虛擬人需要能夠識別經紀人講話並且進行回答或者提問。經紀人講盤訓練整個業務架構如下所示:
其中,自動語音識別技術 ASR(Automatic Speech Recognition),是一種將人的語音轉換爲文本的技術,反之,TTS(Text To Speech),可以將文本轉換爲語音。實時音視頻房間可以使用騰訊雲或者聲網等公司的技術,或者使用 webRTC 進行自研,本文重點關注圖中標色部分的貝殼音頻流網關,其功能爲將經紀人的語音推送到後端 ASR 服務,進行識別和分析,之後通過 TTS 生成一個回答或者提問並通過音頻流網關將語音回傳給經紀人。本文主要介紹音頻流網關的架構、演進、穩定性建設及使用場景。
音頻基礎知識
聲音是振動產生的聲波,經過空氣、固體、液體這些介質傳播並被人或動物聽覺器官所感知的波動現象。音頻信號採集需要將聲音通過麥克風等轉變爲模擬信號,然後對模擬信號進行抽樣、量化和編碼轉換成離散的數字信號。PCM(Pulse Code Modulation)就是一種將模擬信號數字化的方法,一般也用來表示未經過封裝的音頻原始文件。模數轉換過程中涉及三個基本概念:採樣位深、採樣率和通道數。
-
採樣位深:每個採樣點用多少 bit 表示,該值越大,能夠表達振動幅度的精確程度就越高。例如採樣位深爲 16bit,則意味着可以將振動幅度劃分爲 65536 個等級。
-
採樣率:每秒的採樣點數,一般用 Hz 來表示,比如 1s 如果有 48000 個採樣點,則採樣率就是 48kHz。因爲人耳的聽覺範圍是 20Hz-20KHz,根據奈奎斯特採樣定理,模數轉換過程中,採樣頻率大於信號中最高頻率的 2 倍時,採樣後的數字信號可以完整地保留原始信號的信息。因此如果採樣率爲 48kHz,可以完整保留 24kHz 以下頻率的完整音頻信息。
-
通道數:聲音通道個數,常見的爲單通道和雙通道。雙通道可以理解爲單通道數據保存兩份。人左右耳因爲空間位置導致聽到聲音時間不同,雙通道通過播放時模擬這種情況營造聲音從不同方向傳來的空間感。
音頻採樣過程中持續採樣時間稱爲幀長,可以使用 20ms,也可以使用 200ms,時間越短延時越小。假設一次採樣,採樣位深是 16bit,採樣率爲 16kHz,單通道,幀長爲 20ms,使用 PCM,則每幀的大小爲:
幀大小 = 位深 * 採樣率 * 幀長 * 通道/ (1000*8)
= 16 * 16000 * 20 * 1 /(1000*8)
= 640字節
貝殼音頻流網關處理的每幀大小就是 640 字節。
架構介紹
貝殼音頻流網關使用實時音視頻雲平臺提供的服務端 SDK 進入房間、退出房間以及在房間內推拉流,除 SDK 功能外,主要需要解決如下問題:
-
控制進入、退出房間時機
-
分角色拉取,合併以及重傳流
-
有狀態服務橫向擴展方法
-
服務穩定性建設
本文逐個解答這些問題。
架構圖
整體架構爲 Dispatcher-Worker 模式,Dispatcher 通過對外暴露 http 接口管理任務,控制 Worker 啓動停止,以及與 ASR、TTS 服務交互;每個 Worker 都是獨立的進程,調用實時音視頻雲平臺的服務端 SDK 進入房間進行推流和拉流。Dispatcher 與 Worker 使用雙向管道(PIPE)進行通信,例如推拉流的交互,控制信息的交互。貝殼音頻流網關使用 Go 代碼實現,Go 標準庫的 os/exec 包可以直接調起一個外部程序執行,並且生成和外部程序的雙向管道進行通信,os/exec 包的關鍵結構體及方法如下:
type Cmd struct {
Path string // 程序名稱
Args []string // 程序參數
Env []string // 程序環境變量
Dir string // 程序路徑
Stdin io.Reader // 程序標準輸入
Stdout io.Writer // 程序標準輸出
Stderr io.Writer // 程序標準錯誤
...
Process *os.Process //生成的進程
...
}
func Command(name string, arg ...string) *Cmd // 返回一個Cmd結構體
func (c *Cmd) Start() error // 啓動一個新的進程
...
func (c *Cmd) StdinPipe() (io.WriteCloser, error) // 返回一個pipe,該pipe和新進程的標準輸入進行連接
func (c *Cmd) StdoutPipe() (io.ReadCloser, error) // 返回一個pipe,該pipe和新進程的標準輸出進行連接
...
func (c *Cmd) Wait() error // 等待新進程退出
...
Cmd 結構體中的 Process 成員變量代表新生成的進程,通過向 Process 發送信號可以控制 Worker 進程的退出
cmd.Process.Signal(syscall.SIGTERM)
拉流
進入房間
音頻流網關何時生成一個 Worker 進程進入房間取決於具體的業務場景,例如在經紀人訓練場中,當經紀人點擊開始訓練之後需要將虛擬人拉入房間,進行交互。Dispatcher 通過提供進房接口,由業務方根據業務場景靈活的控制開始拉流的時機,接口及返回值信息如下:
接口:POST /task HTTP/1.1
返回值:
{
"errno": 0,
"errmsg": "success",
"data": {
"address": "10.x.x.x:8888"
}
}
可以看到,返回值中還包含了處理該次請求的機器 IP + 端口,這是爲了能夠橫向擴展做的一處優化,跟推流有關係,下文詳細描述
流傳輸
業務方調用進房接口後,Dispatcher 會生成一個 Worker 進入房間開始拉取房間音頻流,之後通過管道傳輸給 Dispatcher,Dispatcher 再傳輸給 ASR 服務。在我們的業務場景中,音頻流每幀是 20ms,16KHz 的採樣率,16bit 的位深,單聲道 PCM 格式,因此每幀大小爲 640 字節,我們做個簡單的計算,假設每個房間中有一個經紀人,同時在線房間數爲 5000(即有 5000 個經紀人同時在線訓練),則每秒鐘的 QPS 爲(1000ms/20ms)*5000 = 250000。爲了提高效率,音頻流網關會默認累積 10 幀(200ms)之後傳輸給 ASR 服務。此時示例中的 QPS 相應降爲 250000/10 = 25000。示意圖如下:
從圖中還可以看出兩點信息:
-
最後一個包可能不滿 10 幀
-
每個包傳輸到 ASR 時會加入一個 Sequence 字段,表明包的順序,最後一個包會將順序字段取負,表明該次拉流已經結束。Sequence 字段會在流重傳時使用。
流重傳
ASR 服務在某些場景丟包之後無法繼續準確識別。因此將包傳輸到 ASR 服務時,除了正常的重試策略,還會在音頻網關保留最近的 30 個包。當 ASR 服務長時間未收到一個包時,可以將該包的 Sequence 返回到音頻流網關,網關啓動時會開啓一定數量的 goroutine,這些 goroutine 收到重傳信息後會從保留包中查找並重傳。
退出房間
退出房間有兩種機制:
-
當房間內有用戶進入和退出時,實時音視頻通道的 SDK 會有回調信息通知此類事件。因此可以通過設置狀態標記和計數器,當房間內所有人都退出後,虛擬人也自動退出
-
提供退房接口,業務方需要退出房間時調用該接口,Dispatcher 給 Worker 進程發送信號,通知其退出。
推流
推流也通過提供一個接口實現,如下:
POST /upstream HTTP/1.1
通過 TTS 生成語音之後,需要以 16bit 位深、16kHz 的採樣率、單聲道 PCM 格式上傳,同時需要指定對應的實時音視頻房間號。上傳成功之後音頻流網關啓動一個 20ms 間隔的 Ticker(Ticker 是 Go 語言中的一個標準庫,經過一個固定時間間隔之後執行某項任務的組件),代碼示例如下:
delayTicker := time.NewTicker(20 * time.Millisecond)
defer delayTicker.Stop()
count := len(alls) //alls中保存待推送的音頻流
var err error
for i := 0; i < count; i++ {
...
<-delayTicker.C // 每20ms會返回一次
err = dispatcher.SendToTrtc(alls[i], e)// 發送音頻流
...
}
間隔 20ms 發送一個音頻包,直到發送完畢
如何找到推流機器
上文講述拉流時,可以看到每個 Worker 會進入一個實時音視頻的房間,而 Worker 是跟具體的機器綁定的有狀態服務。那麼推流時如何找到某個房間號對應的 Worker 屬於哪臺機器呢?
有兩種方法,一是調用 task 接口進入房間時會返回該房間對應的機器 IP+Port,業務方可以記錄該返回信息,推流時直接使用。
第二種方法是音頻流網關提供了一個查詢接口,業務方可以通過查詢接口得到房間號對應的機器 IP+PORT。
演進及優化
心跳機制
第一版上線後發現虛擬人推流時,經紀人端的播放會出現缺失開頭幾個字的吞音現象。查看實時音視頻通道側的用戶事件,發現推流之前會發生經紀人退出房間的現象,原因爲經紀人側認爲房間內已沒有其他人員,因此主動退房。改進機制爲每秒鐘發送一個靜音幀作爲心跳。
內存分配
上文提到每個音頻幀是 640 字節,如果每次我們都直接新生成一個代表音頻幀的結構體,則內存分配與 GC 耗時都會成爲瓶頸,因此需要提前生成一批音頻幀結構體進行復用,放入一個池子,使用時取出,使用完畢後放回。在 Go 語言中,很自然會想到 sync.Pool 標準包。
type Pool struct {
New func() interface{}
}
func (p *Pool) Get() interface{}
func (p *Pool) Put(x interface{})
sync.Pool 包中只有簡單的 Get 與 Put 兩個方法,用來獲取和放回一個結構體;New 成員是一個函數,作用爲生成一個新的結構體。
高可用建設
音頻流網關的高可用建設需要考慮如下幾個方面:
-
入口處:限流
-
強依賴:無,不需考慮降級
-
單次請求生命週期平均爲 5-10 分鐘(即每個 Worker 進程會持續 5-10 分鐘),需要根據單進程佔用資源控制進程的總數量
-
實時音視頻通道:現在使用某雲平臺的實時音視頻通道,是一個單點,多通道建設成本高,暫不考慮
限流
通過使用 Sentinel,配合配置中心下發限流開關以及限流值,達到接口維度的限流
單機容量
音頻流網關爲計算密集型服務,主要資源瓶頸爲 CPU,經過測試單 Worker CPU 使用率爲 5%,因此單 CPU 支持的 Worker 個數爲 20 個。因此根據服務器總的 CPU 核心個數,計算 Worker 的容量值,通過配置中心下發進行限制。
容量狀態
通過提供一個接口,可以觀察音頻流網關當前容量狀態,並且業務方可以據此決定是否需要進行業務側限流,接口如下:
GET /current/workers
{
"errno":0,
"errmsg":"success",
"data":{
"current_nums":807,
"max_total_nums":1620
}
}
上述示例表明當前總的 Worker 個數爲 807,音頻流網關可承載的總的 Worker 個數爲 1620,即使用了接近 50% 的容量
場景
音頻流網關主要用到如下 5 種場景:
-
經紀人講盤訓練:將經紀人的語音推送到後端 ASR 服務,進行識別和分析,之後通過 TTS 生成一個回答或者提問並通過音頻流網關將語音回傳給經紀人。
-
錄製轉碼:拉取音頻流之後可以分別按經紀人和客戶錄製爲單流音頻,也可以將多路單流合併後錄製混流音頻,轉碼之後上傳存儲進行音頻分析和音頻回
-
經紀人提詞器:通過拉取音頻流轉碼爲文本並進行分析,可以生成對應的答案給到經紀人端
-
直播審覈:傳統方式需要審覈人員進入直播間全程觀看,通過音頻流網關可以將多個直播間音頻拉取到 ASR 服務轉換爲文本並通過九宮格方式展示,提高審覈效率
-
網絡語音播報:使用 TTS 服務生成語音後向音頻流網關推流並接通經紀人 APP,可以進行網絡語音播報,例如通知類消息或者提醒類消息
結論
從文字、書籍到電話、短信,進一步到 IM、語音通話、視頻通話,技術的進步使得人與人間的溝通交流更加方便。對企業來說,通過技術賦能,可以讓服務者提供更好的服務,客戶擁有更好的體驗,技術的魅力就在於讓人類生活的更美好吧。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DEeJMGexYNRFSXux59JMhw