在網(wǎng)頁中渲染公式一直是泛學術(shù)工具繞不開的一個功能,最近更新產(chǎn)品功能,正巧遇到了這個需求,于是使用容器方式簡單實現(xiàn)了一個相對靠譜的公式渲染服務。
分享出來,希望能夠幫到有類似需求的同學。
寫在前面
本篇內(nèi)容會分別使用現(xiàn)有開源軟件官方鏡像、定制性能更高的鏡像、進一步搭配 Nginx 來提升整體服務性能以及可靠性。
如果你不熟悉或者不愿意維護 Node 相關(guān)服務,可以將其部署至公有云 Serverless 服務中,搭配緩存服務,更快的獲取產(chǎn)品服務能力,正如軟件描述中所述: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/
接口支持的字段信息在項目文檔中都有,只需根據(jù)自己需求進行調(diào)整即可。為了方便測試,我們這里使用 GET 方式調(diào)用接口,模擬訪問一個能夠動態(tài)渲染圖片的接口。
在服務啟動之后,,使用瀏覽器分別訪問下面的地址:
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
便能看到質(zhì)能方程的公式圖片。
動態(tài)渲染出的質(zhì)能方程公式圖片
如果你是自己個人使用,調(diào)用次數(shù)極少,或者不在意資源消耗可以使用下面的編排文件運行使用。
version: "3.0"
services:
math-api:
restart: always
image: chialab/math-api
ports:
- 3000:3000
logging:
driver: "json-file"
options:
max-size: "1m"
不過如果是要提供公共服務,便需要考慮到各種安全問題、服務性能問題,以及最重要的服務穩(wěn)定性如何。
那么,我們來看看如何提升穩(wěn)定性、并解決基礎(chǔ)安全問題。
思考如何優(yōu)化服務
在優(yōu)化之前,我們先來看看當前國內(nèi)最大的中文社區(qū):知乎,是怎么做的。
我們以 請問你見過的最強的公式是什么? 這篇充滿公式的問題為例,隨便摘取一個公式,觀察圖片內(nèi)容格式:
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 參數(shù)后跟著一堆被轉(zhuǎn)碼后的公式內(nèi)容,我們使用 decodeURIComponent 將其解碼,可以看到 LeTax 公式原本內(nèi)容。
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}
相比較前一小節(jié)中直接在鏈接中傳遞 E=mc^2 展示質(zhì)能方程,如果我們將還原的公式直接拼合到公式接口中,會看到接口報錯(通過接口報錯,我們幾乎可以確定知乎使用的就是類似的方案),這是因為公式中如果包含的 & 字符,那么這個字符前后的內(nèi)容會被切割為不同的參數(shù)傳遞給后端,所以為了避免這類字符在傳遞過程中被錯誤解析,我們一般會將內(nèi)容編碼后進行傳輸。
現(xiàn)在,我們得到了第一個線索:讓參數(shù)編碼后傳輸。
此外,如果我們的使用場景類似知乎,只需要在網(wǎng)頁中展示某個固定的方程,而不需要高度定制這個公式的輸出格式、輸出尺寸,那么可以和知乎一樣,將多數(shù)參數(shù)固化、形成常量配置。
一方面,可以減少開源軟件作者對于各種參數(shù)過濾缺失產(chǎn)生的問題,另外一方面,可以減少服務在運行過程中,被枚舉攻擊而造成資源浪費,甚至服務不可用的可能性,進一步提升服務可靠性和安全性。
那么,我們得到了第二個線索,讓暴露參數(shù)盡可能少。
使用 Nginx 快速優(yōu)化服務
有了前面的兩條線索,我們現(xiàn)在開始優(yōu)化服務。
使用 Nginx 處理網(wǎng)絡請求
結(jié)合前文“公式渲染服務初體驗”小節(jié),和前篇《使用容器搭建簡單可靠的容器倉庫》一文中的配置,不難寫出一個簡單的 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:
這里我們主要做了兩件事:
- 將兩個應用放置相同的容器網(wǎng)絡中。
- 由 Nginx 接受公開的網(wǎng)絡請求,然后再轉(zhuǎn)發(fā)給開源公式應用。
如果你想了解如何使用 Nginx 提供 HTTPS 服務,并盡可能減少代碼,可以翻閱前一篇文章;如果你想了解如何搭配 Traefik 一起提供服務,也可以翻閱之前有關(guān) Traefik 的內(nèi)容,這里不做贅述。
接著我們編寫 Nginx 基礎(chǔ)配置:
server {
listen 80;
# 限制只渲染最大1K數(shù)據(jù),避免服務被惡意攻擊
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 啟動服務。
依舊訪問前文中的本地端口,這次我們可以將公式內(nèi)容替換為前文中知乎公式圖片的內(nèi)容:
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 減少請求參數(shù)
減少參數(shù)可以使用非常多的方式,這里選擇一種最基礎(chǔ)的方案,來自 ngx_http_core_module 的 set args 來強制聲明請求參數(shù):
server {
listen 80;
# 限制只渲染最大1K數(shù)據(jù),避免服務被惡意攻擊
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';
}
}
重新啟動服務,你會發(fā)現(xiàn)上面的請求參數(shù)可以被簡化為下面這樣:
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
那么是不是優(yōu)化就到此為止了呢,顯然不是的,如果我們構(gòu)造有風險的參數(shù)、亦或者接收到了被我們固化的參數(shù),參數(shù)類型產(chǎn)生變化,那么服務還是存在一定的隱患。
比如,我們在定義了 output 參數(shù)后,依舊傳遞了這個參數(shù):
http://localhost:3000/render?output=png&...
則會收到諸如 {"message":"Invalid output: png,svg"} 的錯誤提示。
為了避免這類錯誤,所以我們可以進一步改造上面的配置:
server {
listen 80;
# 限制只渲染最大1K數(shù)據(jù),避免服務被惡意攻擊
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';
}
}
重啟服務,你會發(fā)現(xiàn)即使再構(gòu)造類似下面請求,服務也不會發(fā)生錯誤了。
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
以及,是如果未傳遞公式內(nèi)容請求服務,也會由 Nginx 直接返回一個 404 Not Found,而不是直接將錯誤請求透傳到公式應用。
最后
迄今為止,我們已經(jīng)使用 Nginx 和開源軟件 Math-API 搭建了一個基礎(chǔ)的公式服務。
下一篇文章,我們將進一步調(diào)教 Nginx 和應用容器,在盡可能不編碼的情況下繼續(xù)進行性能調(diào)優(yōu)。






