HelloTalk:基于 OpenResty 和 Apache APISIX 的
HelloTalk:技術上看是基于全球的 Tiny 版微信
HelloTalk 是全球最大的外語學習社交社區,全球 1600 萬用戶通過 HelloTalk 和全球語伴學習 150 門外語、進行跨文化交流及交友。用戶廣泛分布中國、日本、韓國、美、歐、巴西等國家,其中海外用戶占 80%,從技術角度來看 HelloTalk 是一個基于全球的 Tiny 版微信。
HelloTalk 海外有很多 KOL 用戶在 YouTube、Instagram、Twitter 等平臺協助做推廣,知名度較高,產品兼顧聊天、改錯、翻譯等功能,用戶可以邊聊邊語音改文字。其中語音改文字和翻譯支持 100 多種語言。
從運營層面看,很多企業出海時不知道怎樣去做第一步,技術上也同樣面臨這個問題——如何做到出海,并且為全球用戶提供優質的服務。為了讓每個國家的用戶都能擁有更好的使用體驗,這里就要不得不提到 OpenResty 給我們帶來的幫助了。
如上圖所示,HelloTalk 的用戶分布很散,我們需要找到最佳的平衡點,以性價比最優的方式部署連接節點。亞太區域如韓國、日本,中國長三角和珠三角等地用戶分布比較集中,比較好處理,但是在用戶高度分散的其他地區(如上圖的歐洲、非洲、中東),對提供穩定可靠的服務提出了較高的挑戰。
為什么使用 OpenResty
早期 HelloTalk 使用 C++ 寫 IM 服務,當時是用到某大廠的高性能網絡框架,協議都是內部擬定的,HTTP 協議都很少用。這對小公司而言成本很高,假設內部寫服務要曝露給外部使用,還需要自己開發 Proxy Server,而最新加的命令字要做適配,這就非常麻煩了。
所以從 2015 年開始,HelloTalk 開始引進 OpenResty,基于 OpenResty 在前面做代理,直接進行協議轉換傳到內部服務,減少了很多的成本。
此外,服務簡單暴露給外部使用,會需要 WAF 的功能。我們早期有些 API 是基于 PHP 實現的,經常會因為框架原因被發現一些漏洞,導致一些黑客做各種注入和攻擊,其中主要的手法就是 POST 各種 PHP 的關鍵字,或者在 URL 里面攜帶 PHP 關鍵字。
當時我們在 OpenResty 里添加很少的代碼(基于正則)后解決了這個問題,后來發現即使增加 WAF 功能,性能上也不會有太大的損失。
- TLV:0x09 + Header(20 bytes) + Body + 0x0A
早期我們做 IM 開發都希望協議短小精悍,HelloTalk 的協議頭也比較精簡,全部是 TCP 的協議體。比較有意思的是通過前后加兩個特殊的字節符號,定義中間的內容,即 0x09 + Header(20 bytes) + Body + 0x0A,基本上可以保證數據包不會出亂。如果沒有前后 0x09 和 0x0A 兩個包,其實還是有一定概率會產生錯包的。
- 自定協議 -> HTTP 的研發成本,急需高效的 proxy 服務做協議轉換
早期 HelloTalk 采用 TLV + PB 的協議模式,當時業務正快速發展,需要改成對外的 restful + JSON,第一步要做的是 PB 轉 JSON。
而做協議解析遇到一個問題:OpenResty 使用的是云風寫的 PBC 解析器,解析寫起來非常麻煩,必須要知道里層的結構。假設結構有三層,你得寫三層判斷代碼,一層一層地把它拋出來。但后來發現 Apache APISIX 是基于 lua-protobuf,所以我們也改成了用 lua-protobuf 庫,它可以直接把一個 PB 對象直接轉成了 JSON,非常方便。
- 基于 cosocket 的 TCP 協議安全解析
協議的解析過程基本上是不斷地讀 socket,讀到上圖中的包頭里的 Length 字段,再去讀 body 段,這里可以看出自己要實現協議解析比較麻煩,因為要對每個協議做適配。
- 快速實現一個 Web IM
我們當時做完 C++ 的 IM 通訊服務后,看到主流的 IM App 如 WhatsApp、微信都有 Web IM,我們很快的基于 OpenResty 對他們的協議進行兼容和改造,大概兩周時間,我們就從服務端快速實現了一個 WebIM 版本的 HelloTalk。
和微信網頁版本一樣掃描登錄聊天,基本不對協議做改動,只在中間添加一層 OpenResty 做 WebSocket 協議轉換。
- 控制消息頻率
公共服務如果暴露出去,會有人頻繁地給所有的人發消息,因此我們需要做消息限流,這是直接基于 resty.limit.req 做的,當然 API 頻率控制也是如此進行的。
- WAF 保護 PHP 業務
做過 PHP 開發應該知道,所有的入侵其實是各種注入 PHP 的函數名字、關鍵字。但當我把所有的 PHP 的函數名全放在 WAF 后,我再也沒發現過被攻擊,但在日志里發現很多,這說明全部被攔截了,到不了 PHP 那邊。
三步走:
- 純 TCP 協議快速實現;
- 基于 Openresty 的 HTTP 服務暴露;
- API網關(Apache APISIX) 加 Golang 微服務開發和治理。
國際化過程中的挑戰和問題
- HelloTalk 用戶分布區域非常分散,需要想辦法解決用戶分布區域分散的問題;
- HelloTalk 國內大概有 20% 的用戶,面臨防火墻的問題;
- 海外語言環境和網絡環境一樣復雜,語言適配問題難以處理。
怎樣提高用戶的全球接入質量
我比較過市面上很多服務商提供的方案:
- 阿里云全球加速 (BGP + 專線),直接就是 4 層加速。
- 阿里云 DCDN 全站加速。
- AWS 的 Global Accelerator 方案。
- Ucloud 的 XPath 方案 。
- 專線服務(兩端 VPC,中間專線,邊緣卸載 https)
- Zenlayer。
但我們需要考慮兩個問題:成本,真正的服務質量。
在解決跨境問題時,由于要考慮到國內 20% 的用戶和公司總部地理位置,所以我們是基于阿里云全站加速展開,原本是全部用公網代理到香港阿里云,采用兩邊是 VPC、中間專線的形式,但有時候會遇到專線網絡抖動導致延時提高的問題,所以在深圳做了基于 OpenResty 的網關代理。而實際情況是:如果專線不通就選擇走公網,公網延時大概 14ms,專線是 4ms。
這里會涉及到上游檢測,線路不通時需要快速的切換到另外一條線路,這部分問題是基于又拍云提供的 Resty 庫在解決。
香港阿里機房到香港騰訊騰訊機房感覺其實是在同一個區域,因為我們測試延時大概在 0.3ms~0.4ms。
對于海外其他用戶,基本全部是直接加速回到香港阿里,但直接加速會導致客戶端的網絡質量受地域問題影響嚴重,所以我們設置了一些 failover 的機制來保障用戶的使用體驗。
接入線路控制和流量管理
- 專線網絡的帶來的穩定性,例如歐洲到香港,延時: 244 ms -> 150 ms;
- 動態 upstream 控制 (lua-resty-healthcheck),多服務商線路之間靈活切換,保證服務的可靠性;
- 部分邏輯可以直接在邊緣處理,serverless(原理都是基于 pcall + loadstring 實現),serverless 這塊我們現在正則將其改造成 Apsche APISIX + ETCD。
接入節點和質量把控
目前 HelloTalk 的接入節點主要分布在:美國東部,法蘭克福,新加坡,東京,香港。美國直接到香港有可能會不通,此時會按照既定機制經轉德國再回到香港,日本和韓國也是回到香港。巴西也有很多用戶,但巴西云廠商只有 AWS 在做,基本上全部是連到美國,如果連不通也會多個線路之間做選擇。這個環節其實是云廠商或者是 CDN 廠商完成,但實際發現總有一些地區做的并不好,所以為了保證用戶體驗不受損,我們得有些 failover 機制保證多個服務商之間切換,保證用戶的服務是可靠的。
7 層和 4 層加速的選擇
很多服務商會提供 7 層加速和 4 層加速,但也會有一些問題需要解決。
- 4 層加速:SSL 握手時間過長,容易失敗,得不到客戶端的 IP,不方便做節點質量統計。
4 層加速得不到客戶端的 IP,(注:有些云廠商是支持的但需要在服務器上打個補丁),它在 TCP 的包里提供了此功能,也不是很友好,如果打補丁出了問題,誰來負這個責任呢?
此外,監控質量也成了問題,我們需要知道哪條線路行、哪條線路不行,雖然有切換機制,但我們要知道它真實的通訊路線。事實上我們在每個流量層代理時都會把真實 IP 帶著跑,如果采用阿里云,那阿里云會幫我們填到一個頭里面去,不斷地把客戶端的真實 IP 帶給下一個節點。
- 7 層加速:不能保證 IM 服務需要長連接保持消息的可靠到達
7 層加速的問題在于使得 IM 服務機制變成了 long polling 或者是短連接輪循機制,但在實際過程中我們發現它比較耗流量,而且 IM 服務需要長連接保持消息的可靠和及時到達,但大部分 7 層加速廠商不支持 WebSocket,個別支持 WebSocket 的廠商邊緣卸載 HTTPS 又很貴的,尤其是國外的像 AWS 挺貴的。此外,如果云廠商邊緣節點宕機,會對用戶造成比較差的影響,因此我們就在多個云廠商之間的客戶端做了很多 failover 邏輯設計(內置 IP 機制),一旦故障能夠切實保障切換到另外一個節點,保證連接質量。
多云環境下的全球接入的管理方案
- 支持 websocket 的 7 層加速。(云服務+自建)
- 自建低速率的 VPC + 專線通道。(性價比考慮,IM 自身流量并不多,只做通知消息下發)
- 長短連接混合收發消息:websocket + long polling + httpdns + 內置 IP failover 機制
當然內置哪個 IP 到客戶端也是一個問題,比如對于歐洲用戶,其實肯定是要分配歐洲的 IP,那么首先我們服務端要把歐洲的服務端 IP 存起來,怎么存?什么時候存?這里我們是通過騰訊云的 httpdns + openresty timer 機制分配、緩存、更新的,上圖中的 IP 就是用戶的真實 IP,這個時候 httpdns 服務商就會根據 IP 參數做域名的 IP 解析。
從自建 API Gateway 到深入體驗 Apache APISIX
自建 API Gateway 實現偽裝動態化
我們早期是直接改 nginx.conf,我自己覺得裸的 nginx 性能肯定是最高的。但問題是很多人不一定記得 Location 配制的優先級順序規則,我們也經常會改錯。而且我們的需求比較固定:動態更新 SSL 證書、Location、upstream,當時的做法類似現在的 K8S 的 ingress 更新機制,即通過模本生成:nginx_template.conf + JSON -> PHP -> nginx.conf -> PHP-cli -> Reload 來實現動態化。但這個方案在遇到 Apache APISIX 之后可以考慮替換掉了。
Apache APISIX 成為 HelloTalk 的選擇
- 自身需求比較簡單,依賴 RDMS 覺得太重,帶來額外的維護成本;
- 代碼極致簡單易懂,在個人能力范圍內,可以理解;
- 基于 ETCD,節省維護成本;
- 項目主要維護者幾乎實時在線支持,QQ 群、郵件響應及時。