在網頁中渲染公式一直是泛學術工具繞不開的一個功能,最近更新產品功能,正巧遇到了這個需求,于是使用容器方式簡單實現了一個相對靠譜的公式渲染服務。
分享出來,希望能夠幫到有類似需求的同學。
寫在前面
本篇內容會分別使用現有開源軟件官方鏡像、定制性能更高的鏡像、進一步搭配 Nginx 來提升整體服務性能以及可靠性。
如果你不熟悉或者不愿意維護 Node 相關服務,可以將其部署至公有云 Serverless 服務中,搭配緩存服務,更快的獲取產品服務能力,正如軟件描述中所述:Serverless API to render maths using MathJax for Node。
公式渲染服務初體驗
我們先啟動一個開源軟件 Math-API 的官方鏡像容器實例,來先體驗一下使用接口渲染公式。
Docker run --rm -it -p 3000:3000 chialab/math-api
yarn run v1.5.1
$ node bin/server.js
Server running at http://localhost:3000/
接口支持的字段信息在項目文檔中都有,只需根據自己需求進行調整即可。為了方便測試,我們這里使用 GET 方式調用接口,模擬訪問一個能夠動態渲染圖片的接口。
在服務啟動之后,,使用瀏覽器分別訪問下面的地址:
http://localhost:3000/render?input=latex&inline=0&output=svg&width=256&source=E=mc^2
http://localhost:3000/render?input=latex&inline=0&output=png&width=256&source=E=mc^2
便能看到質能方程的公式圖片。
動態渲染出的質能方程公式圖片
如果你是自己個人使用,調用次數極少,或者不在意資源消耗可以使用下面的編排文件運行使用。
version: "3.0"
services:
math-api:
restart: always
image: chialab/math-api
ports:
- 3000:3000
logging:
driver: "json-file"
options:
max-size: "1m"
不過如果是要提供公共服務,便需要考慮到各種安全問題、服務性能問題,以及最重要的服務穩定性如何。
那么,我們來看看如何提升穩定性、并解決基礎安全問題。
思考如何優化服務
在優化之前,我們先來看看當前國內最大的中文社區:知乎,是怎么做的。
我們以 請問你見過的最強的公式是什么? 這篇充滿公式的問題為例,隨便摘取一個公式,觀察圖片內容格式:
https://www.zhihu.com/equation?tex=%5Cbegin%7Balign%7D%26%5Cprod_%7Bn%3D1%7D%5E%5Cinfty%5Cfrac%7B%28n%2Ba_1%29%28n%2Ba_2%29...%28n%2Ba_k%29%7D%7B%28n%2Bb_1%29%28n%2Bb_2%29...%28n%2Bb_k%29%7D%5C%5C%26%3D%5Cfrac%7B%5CGamma%281%2Bb_1%29%5CGamma%281%2Bb_2%29...%5CGamma%281%2Bb_k%29%7D%7B%5CGamma%281%2Ba_1%29%5CGamma%281%2Ba_2%29...%5CGamma%281%2Ba_k%29%7D%5Cend%7Balign%7D
可以看到鏈接 tex 參數后跟著一堆被轉碼后的公式內容,我們使用 decodeURIComponent 將其解碼,可以看到 LeTax 公式原本內容。
decodeURIComponent('%5Cbegin%7Balign%7D%26%5Cprod_%7Bn%3D1%7D%5E%5Cinfty%5Cfrac%7B%28n%2Ba_1%29%28n%2Ba_2%29...%28n%2Ba_k%29%7D%7B%28n%2Bb_1%29%28n%2Bb_2%29...%28n%2Bb_k%29%7D%5C%5C%26%3D%5Cfrac%7B%5CGamma%281%2Bb_1%29%5CGamma%281%2Bb_2%29...%5CGamma%281%2Bb_k%29%7D%7B%5CGamma%281%2Ba_1%29%5CGamma%281%2Ba_2%29...%5CGamma%281%2Ba_k%29%7D%5Cend%7Balign%7D')
begin{align}&prod_{n=1}^inftyfrac{(n+a_1)(n+a_2)...(n+a_k)}{(n+b_1)(n+b_2)...(n+b_k)}\&=frac{Gamma(1+b_1)Gamma(1+b_2)...Gamma(1+b_k)}{Gamma(1+a_1)Gamma(1+a_2)...Gamma(1+a_k)}end{align}
相比較前一小節中直接在鏈接中傳遞 E=mc^2 展示質能方程,如果我們將還原的公式直接拼合到公式接口中,會看到接口報錯(通過接口報錯,我們幾乎可以確定知乎使用的就是類似的方案),這是因為公式中如果包含的 & 字符,那么這個字符前后的內容會被切割為不同的參數傳遞給后端,所以為了避免這類字符在傳遞過程中被錯誤解析,我們一般會將內容編碼后進行傳輸。
現在,我們得到了第一個線索:讓參數編碼后傳輸。
此外,如果我們的使用場景類似知乎,只需要在網頁中展示某個固定的方程,而不需要高度定制這個公式的輸出格式、輸出尺寸,那么可以和知乎一樣,將多數參數固化、形成常量配置。
一方面,可以減少開源軟件作者對于各種參數過濾缺失產生的問題,另外一方面,可以減少服務在運行過程中,被枚舉攻擊而造成資源浪費,甚至服務不可用的可能性,進一步提升服務可靠性和安全性。
那么,我們得到了第二個線索,讓暴露參數盡可能少。
使用 Nginx 快速優化服務
有了前面的兩條線索,我們現在開始優化服務。
使用 Nginx 處理網絡請求
結合前文“公式渲染服務初體驗”小節,和前篇《使用容器搭建簡單可靠的容器倉庫》一文中的配置,不難寫出一個簡單的 docker-compose.yml ,容器編排配置文件:
version: "3.0"
services:
nginx:
image: nginx:1.19.8-alpine
restart: always
ports:
- 3000:80
volumes:
- ./default.conf:/etc/nginx/conf.d/default.conf
networks:
- formula
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy off localhost/get-health || exit 1"]
interval: 10s
timeout: 1s
retries: 3
logging:
driver: "json-file"
options:
max-size: "1m"
math-api:
restart: always
image: chialab/math-api
expose:
- 3000
networks:
- formula
logging:
driver: "json-file"
options:
max-size: "1m"
networks:
formula:
這里我們主要做了兩件事:
- 將兩個應用放置相同的容器網絡中。
- 由 Nginx 接受公開的網絡請求,然后再轉發給開源公式應用。
如果你想了解如何使用 Nginx 提供 HTTPS 服務,并盡可能減少代碼,可以翻閱前一篇文章;如果你想了解如何搭配 Traefik 一起提供服務,也可以翻閱之前有關 Traefik 的內容,這里不做贅述。
接著我們編寫 Nginx 基礎配置:
server {
listen 80;
# 限制只渲染最大1K數據,避免服務被惡意攻擊
client_max_body_size 1k;
access_log off;
location / {
proxy_pass http://math-api:3000;
}
location = /get-health {
access_log off;
default_type text/html;
return 200 'alive';
}
}
將配置保存為 default.conf,然后使用 docker-compose up 啟動服務。
依舊訪問前文中的本地端口,這次我們可以將公式內容替換為前文中知乎公式圖片的內容:
http://localhost:3000/render?input=latex&inline=0&output=svg&width=256&source=%5Cbegin%7Balign%7D%26%5Cprod_%7Bn%3D1%7D%5E%5Cinfty%5Cfrac%7B%28n%2Ba_1%29%28n%2Ba_2%29...%28n%2Ba_k%29%7D%7B%28n%2Bb_1%29%28n%2Bb_2%29...%28n%2Bb_k%29%7D%5C%5C%26%3D%5Cfrac%7B%5CGamma%281%2Bb_1%29%5CGamma%281%2Bb_2%29...%5CGamma%281%2Bb_k%29%7D%7B%5CGamma%281%2Ba_1%29%5CGamma%281%2Ba_2%29...%5CGamma%281%2Ba_k%29%7D%5Cend%7Balign%7D
針對復雜公式的渲染
可以看到圖片渲染的“非常漂亮”。
使用 Nginx 減少請求參數
減少參數可以使用非常多的方式,這里選擇一種最基礎的方案,來自 ngx_http_core_module 的 set args 來強制聲明請求參數:
server {
listen 80;
# 限制只渲染最大1K數據,避免服務被惡意攻擊
client_max_body_size 1k;
access_log off;
location / {
set $args $args&input=latex&inline=0&output=svg&width=256;
proxy_pass http://math-api:3000;
}
location = /get-health {
access_log off;
default_type text/html;
return 200 'alive';
}
}
重新啟動服務,你會發現上面的請求參數可以被簡化為下面這樣:
http://localhost:3000/render?source=%5Cbegin%7Balign%7D%26%5Cprod_%7Bn%3D1%7D%5E%5Cinfty%5Cfrac%7B%28n%2Ba_1%29%28n%2Ba_2%29...%28n%2Ba_k%29%7D%7B%28n%2Bb_1%29%28n%2Bb_2%29...%28n%2Bb_k%29%7D%5C%5C%26%3D%5Cfrac%7B%5CGamma%281%2Bb_1%29%5CGamma%281%2Bb_2%29...%5CGamma%281%2Bb_k%29%7D%7B%5CGamma%281%2Ba_1%29%5CGamma%281%2Ba_2%29...%5CGamma%281%2Ba_k%29%7D%5Cend%7Balign%7D
那么是不是優化就到此為止了呢,顯然不是的,如果我們構造有風險的參數、亦或者接收到了被我們固化的參數,參數類型產生變化,那么服務還是存在一定的隱患。
比如,我們在定義了 output 參數后,依舊傳遞了這個參數:
http://localhost:3000/render?output=png&...
則會收到諸如 {"message":"Invalid output: png,svg"} 的錯誤提示。
為了避免這類錯誤,所以我們可以進一步改造上面的配置:
server {
listen 80;
# 限制只渲染最大1K數據,避免服務被惡意攻擊
client_max_body_size 1k;
access_log off;
location / {
if ( $arg_source = '') {
return 404;
}
set $args source=$arg_source&input=latex&inline=0&output=svg&width=256;
proxy_pass http://math-api:3000;
}
location = /get-health {
access_log off;
default_type text/html;
return 200 'alive';
}
}
重啟服務,你會發現即使再構造類似下面請求,服務也不會發生錯誤了。
http://localhost:3000/render?output=png&source=%5Cbegin%7Balign%7D%26%5Cprod_%7Bn%3D1%7D%5E%5Cinfty%5Cfrac%7B%28n%2Ba_1%29%28n%2Ba_2%29...%28n%2Ba_k%29%7D%7B%28n%2Bb_1%29%28n%2Bb_2%29...%28n%2Bb_k%29%7D%5C%5C%26%3D%5Cfrac%7B%5CGamma%281%2Bb_1%29%5CGamma%281%2Bb_2%29...%5CGamma%281%2Bb_k%29%7D%7B%5CGamma%281%2Ba_1%29%5CGamma%281%2Ba_2%29...%5CGamma%281%2Ba_k%29%7D%5Cend%7Balign%7D
以及,是如果未傳遞公式內容請求服務,也會由 Nginx 直接返回一個 404 Not Found,而不是直接將錯誤請求透傳到公式應用。
最后
迄今為止,我們已經使用 Nginx 和開源軟件 Math-API 搭建了一個基礎的公式服務。
下一篇文章,我們將進一步調教 Nginx 和應用容器,在盡可能不編碼的情況下繼續進行性能調優。