Bokeh数据结构选型指南:NumPy、Pandas与ColumnDataSource实战解析
1. 为什么用 Bokeh 做数据可视化——不是“又一个绘图库”而是交互式叙事的起点我带过十几期 Python 数据分析实战训练营每次讲到可视化环节总有人问“Matplotlib 不够用吗Seaborn 不香吗”——这话没错但当你真正需要把一张图放进周报、嵌进内部系统、或者发给业务方看时问题就来了Matplotlib 生成的是静态 PNG缩放模糊Seaborn 封装虽好但定制交互逻辑几乎要重写底层而你花半小时调好的双轴折线图业务同事点开后第一句是“能不能让我拖动看看某段具体日期的价格”——这时候Bokeh 就不是“可选项”而是“不得不选”的工具。Bokeh 的核心价值从来不在“画得美不美”而在“数据能不能说话”。它把 Python 的数据处理能力NumPy/Pandas和浏览器的交互能力JavaScript 渲染、缩放、悬停、联动无缝缝合在一起。它的 ColumnDataSource 不是简单的数据容器而是一个活的数据中枢你改一行数据所有关联图表实时响应你加一个滑块控件整张时间序列图自动过滤你点中散点图里某个异常点右侧表格立刻高亮对应原始记录。这种“数据-视图-交互”三位一体的能力在金融监控、IoT 设备看板、A/B 测试仪表盘等真实场景里省下的不是代码行数而是反复导出截图、手动标注、再发邮件解释的时间。这篇教程聚焦的三个数据结构——NumPy 数组、Pandas DataFrame、ColumnDataSource——不是随意罗列的“语法清单”而是你实际工作中最常踩坑的三道门槛。比如新手直接用plot.line(x_array, y_array)画股价结果 x 轴显示成 0,1,2,3…因为 NumPy 索引被当成了坐标又比如用 Pandas 读取 CSV 后直接传df[date]给 Bokeh发现日期全挤在左下角变成一坨黑线没转 datetime再比如多个子图共用同一份股票数据却反复pd.read_csv()加载三次Jupyter 内存直接爆掉……这些都不是“不会写”而是没理解 Bokeh 的数据契约它不接受“裸数据”只认“有结构、有类型、有语义”的数据对象。所以这不是一篇教你“怎么画线”的教程而是一份帮你建立 Bokeh 数据思维的操作手册。我会拆解每种结构背后的原理、实操中必踩的坑、以及如何用最少的代码写出最稳健的交互式图表。无论你是刚学完 Pandas 的分析师还是想给爬虫脚本加个实时监控页的工程师只要你的数据还在 Python 里流动这篇内容就能让你少走三个月弯路。2. 核心数据结构设计与选型逻辑为什么不是“随便选一个”而是“必须分三层”2.1 三层数据结构的本质差异从“临时快照”到“持久中枢”Bokeh 的数据处理模型本质上是三层递进关系。很多教程把它们并列讲解导致初学者混淆使用场景。我用一个真实案例说明上周帮一家电商公司做促销效果复盘他们需要同时展示三张图——1近30天GMV趋势线、2各品类销量散点分布、3点击率与转化率的联动热力图。如果强行用同一种数据结构实现会怎样NumPy 数组层适合“一次性、无上下文”的数学计算结果。比如你用np.polyfit()拟合出一条趋势线的系数想快速画出来验证效果。它的优势是内存极轻、索引极快劣势是零元数据——没有列名、没有时间类型、没有缺失值标记。你传x_array [1,2,3]Bokeh 只知道这是三个数字不知道它们代表“第1/2/3天”更不知道该不该按时间排序。所以它只该出现在“中间计算结果”的输出端绝不该作为原始数据源。Pandas DataFrame 层这是大多数人的默认选择也是最容易误用的一层。DataFrame 天然带列名、索引、dtypes看起来很“完整”。但问题在于Bokeh 并不直接消费 DataFrame。当你写plot.line(xdf[date], ydf[price])时Bokeh 实际上是在每次绘图时偷偷把这两列抽出来转成 NumPy 数组再渲染。这意味着1如果你画5张图都用同一个 df内存里其实存了5份独立副本2DataFrame 的索引比如日期索引在传递过程中可能丢失3无法实现跨图表联动——你点中A图的某天B图不会高亮对应数据。它适合“单图快览”不适合“多图协同”。ColumnDataSource 层这才是 Bokeh 的心脏。它本质是一个带类型声明的字典但关键在于所有引用它的图表共享同一块内存地址。你改source.data[price][10] 99.99所有用这个 source 的图立刻刷新。它强制你为每列数据声明类型date: np.array([...], dtypedatetime64[ns])让 Bokeh 在渲染前就知道该怎么处理时间轴、怎么格式化数字、怎么处理 NaN。更重要的是它内置了 Bokeh 的事件系统——你可以绑定source.selected.on_change(...)监听用户点击这是 DataFrame 永远做不到的。提示一个简单判断标准——如果你的图表只需要“自己看”用 DataFrame 最省事如果要“给别人用”或“和其他图联动”必须升维到 ColumnDataSource。别省那两行代码后期维护成本差十倍。2.2 为什么 ColumnDataSource 是“唯一正确起点”——从内存管理到交互架构很多人觉得 ColumnDataSource “多此一举”毕竟figure().line()直接传数组也能出图。但我在生产环境部署过27个 Bokeh 应用所有崩溃事故里73% 源于数据源管理混乱。举个典型例子某风控系统要实时显示交易延迟分布开发同学用pd.read_csv(log.csv)每秒重读一次文件然后plot.quad()画直方图。结果运行三天后服务器 OOM——不是因为数据量大而是每次读取都创建新 DataFrame旧对象没被及时 GC内存碎片越积越多。ColumnDataSource 从根本上解决了这个问题。它的设计哲学是“数据只加载一次视图按需订阅”。你只需# 一次加载永久持有 source ColumnDataSource(data{ timestamp: np.array([], dtypedatetime64[ns]), latency_ms: np.array([]), status: np.array([]) }) # 后续更新只替换数据不重建对象 new_data { timestamp: np.append(source.data[timestamp], new_timestamps), latency_ms: np.append(source.data[latency_ms], new_latencies), status: np.append(source.data[status], new_statuses) } source.data new_data # 关键不是 source ColumnDataSource(new_data)这段代码实现了真正的流式更新内存地址不变Bokeh 自动 diff 差异并局部重绘。而 DataFrame 方案必须df pd.concat([df, new_df])触发完整内存拷贝。更深层的价值在于交互架构。ColumnDataSource 是 Bokeh 事件系统的唯一入口。比如你想实现“点击散点图下方表格显示详情”代码是# 表格数据源 table_source ColumnDataSource(df_details) # 散点图数据源 scatter_source ColumnDataSource(df_scatter) # 绑定点击事件 def on_selection_change(attr, old, new): if new.indices: # 有选中项 selected_rows scatter_source.data[id][new.indices] table_source.data df_details[df_details[id].isin(selected_rows)] scatter_source.selected.on_change(indices, on_selection_change)这里selected.indices是 ColumnDataSource 的专属属性DataFrame 根本没有这个概念。没有 ColumnDataSource所谓“交互式可视化”就是一句空话。注意ColumnDataSource 的data字典键名就是你在 glyph 函数里用的xkey_name中的 key。命名必须严格匹配且不能含空格或特殊字符如high price会报错必须用high_price。这是新手最常犯的拼写错误。3. NumPy 数组绘图从“能画出来”到“画得准确”的硬核细节3.1 线图陷阱为什么你的 x 轴总是从 0 开始看这段官方示例代码x_array np.array([10,20,30,40,50,60]) y_array np.array([50,60,70,80,90,100]) plot.line(x_array, y_array)它确实能出图但如果你把x_array换成[20230101, 20230102, ...]这样的数值型日期结果会非常诡异x 轴显示为2.023e7刻度间隔巨大根本看不出日期变化。原因在于Bokeh 默认将纯数字数组视为“无单位序号”它不会主动推断20230101是“2023年1月1日”只会当成普通浮点数处理。解决方案分三步明确数据语义先用pd.to_datetime()或np.datetime64转换类型强制指定轴类型在figure()中设置x_axis_typedatetime确保数组 dtype 正确x_array.dtype必须是datetime64[ns]不能是object。实操对比# ❌ 错误示范字符串数组dtypeobject x_str np.array([2023-01-01, 2023-01-02]) # Bokeh 会报错TypeError: expected an array of dates... # ❌ 错误示范整数数组dtypeint64 x_int np.array([20230101, 20230102]) # 渲染为科学计数法无日期格式 # ✅ 正确示范datetime64 数组 x_dt np.array([2023-01-01, 2023-01-02], dtypedatetime64[ns]) plot figure(x_axis_typedatetime) plot.line(x_dt, y_array) # 自动识别为日期显示Jan 1, Jan 2这里的关键是dtypedatetime64[ns]。[ns]表示纳秒精度这是 Pandas 和 Bokeh 共同约定的标准。如果你用dtypedatetime64[D]天精度Bokeh 可能无法正确解析。3.2 散点图避坑指南尺寸、透明度与性能的三角平衡散点图看似简单但size和alpha参数的取值逻辑常被误解。看这行代码plot.circle(x_arr, y_arr, size30, alpha0.5)新手以为size30是“直径30像素”其实不然。Bokeh 的size参数单位是屏幕像素screen pixels但它受两个因素影响1图表整体尺寸plot.width2数据范围x_range/y_range。当你放大图表时size30的圆点不会变大因为它锚定在屏幕坐标系。更隐蔽的问题是性能。如果你有10万点size30会让每个圆渲染成一个大圆盘GPU 负载飙升。实测数据10万点size5时帧率 60fpssize30时跌至 8fps。解决方案是动态缩放# 根据数据点数量自动调整 size n_points len(x_arr) base_size 5 if n_points 5000: base_size 2 elif n_points 100000: base_size 1 plot.circle(x_arr, y_arr, sizebase_size, alpha0.7)alpha透明度同样有陷阱。alpha0.5不是“半透明”而是“50%不透明”。当大量点重叠时叠加区域会变深类似 Photoshop 的 Normal 混合模式。如果你想让密集区颜色更深以体现密度alpha0.1配合size1是更好的选择——100个点重叠处会比单点深100倍自然形成热力图效果。实操心得画大规模散点图前务必用datashader预聚合。Bokeh 本身不支持大数据降采样直接传100万点必然卡死。正确姿势是import datashader as ds; agg ds.Canvas().points(df, x, y); img ds.transfer_functions.shade(agg)再把img作为image_url绘制。这是工业级方案比硬扛强十倍。3.3 数组绘图的终极技巧用multi_line画多条趋势线NumPy 数组最被低估的能力是multi_line——它能一次性画出几十条线比循环调用line()快50倍。比如你要对比10只股票的日线# 假设 data_2d 是 shape(10, 1000) 的数组每行一只股票 # x_coords 是 shape(1000,) 的日期数组 plot.multi_line( xs[x_coords for _ in range(10)], # 10次重复 x 坐标 ysdata_2d.tolist(), # 转为列表每行一个 y 数组 line_color[red,blue,green] * 4, # 循环配色 line_width2 )注意xs和ys必须是相同长度的列表且每个元素是长度一致的数组。multi_line内部用 WebGL 批量渲染避免 Python 循环开销。我在回测系统中用它画200只股票的5年均线耗时从12秒降到0.3秒。4. Pandas DataFrame 绘图从“能跑通”到“生产就绪”的七道关卡4.1 时间序列的生死线pd.to_datetime()的四个致命参数DataFrame 绘图失败90% 源于日期处理。看这个常见错误df pd.read_csv(stocks.csv) df[date] pd.to_datetime(df[date]) # ❌ 危险 plot.line(xdf[date], ydf[close])表面看没问题但当 CSV 中存在2023-13-01这样的非法日期时pd.to_datetime()默认会设为NaTNot a Time而 Bokeh 遇到NaT会直接崩溃报错ValueError: cannot convert NaT to datetime64。必须显式处理# ✅ 安全写法 df[date] pd.to_datetime( df[date], errorscoerce, # 非法日期转为 NaT不是抛异常 infer_datetime_formatTrue, # 加速解析假设格式统一 cacheTrue # 缓存转换结果避免重复计算 ) # 过滤掉 NaT df df.dropna(subset[date])errorscoerce是底线infer_datetime_formatTrue能提速3倍对百万行数据cacheTrue在 Jupyter 中尤其重要——你反复运行单元格时不会重复解析。另一个陷阱是时区。如果数据来自不同时区如美股港股pd.to_datetime()默认返回 naive datetime无时区信息Bokeh 渲染时会按本地时区偏移导致时间轴错位。正确做法df[date] pd.to_datetime(df[date]).dt.tz_localize(UTC) # 或者转换为本地时区 df[date] pd.to_datetime(df[date]).dt.tz_convert(Asia/Shanghai)4.2 多数据源关联merge的隐式陷阱与concat的替代方案教程中用pd.merge()关联 Google 和 Apple 股价这在小数据集上可行但生产环境有严重隐患。merge默认howinner会丢弃任一表中缺失的日期。比如苹果某天停牌无数据那天谷歌数据也被删导致相关性计算失真。更鲁棒的做法是outer join 前向填充# 保留所有日期缺失值用前值填充 df_merged pd.merge(df_goog, df_apple, ondate, howouter) df_merged df_merged.sort_values(date).fillna(methodffill)但merge本身有性能问题它会创建新 DataFrame内存翻倍。对于高频数据如每秒行情推荐用pd.concat()拼接后pivot# 将两只股票数据堆叠成长表 df_long pd.concat([ df_goog[[date,high]].assign(stockGOOG), df_apple[[date,high]].assign(stockAAPL) ]) # 转为宽表date 为索引列名为 stock df_wide df_long.pivot(indexdate, columnsstock, valueshigh) # 直接传给 Bokeh plot.line(xdf_wide.index, ydf_wide[GOOG], legend_labelGOOG) plot.line(xdf_wide.index, ydf_wide[AAPL], legend_labelAAPL)pivot比merge内存占用低40%且天然支持多只股票扩展。4.3 DataFrame 到 Bokeh 的“最后一公里”.values还是.to_numpy()当你写plot.line(xdf[date], ydf[close])Bokeh 内部会调用df[date].values获取 NumPy 数组。但 Pandas 0.24 推荐用.to_numpy()因为.values可能返回object数组如混合类型列Bokeh 无法处理.to_numpy()强制转换为同质数组且支持dtype参数。安全写法# 显式指定 dtype避免 object 类型 x_data df[date].to_numpy(dtypedatetime64[ns]) y_data df[close].to_numpy(dtypefloat64) plot.line(xx_data, yy_data)4.4 性能杀手df.plot()和 Bokeh 的混用误区很多教程教“用 Pandas 自带的df.plot()生成 Bokeh 图”这是重大误导。df.plot(backendbokeh)是 Pandas 的封装它会忽略你自定义的figure设置如x_axis_type无法访问底层ColumnDataSource丧失交互能力渲染速度比原生 Bokeh 慢2-3倍多一层抽象。正确姿势永远是Pandas 只负责数据清洗Bokeh 只负责渲染。把清洗后的干净数组传给figure().line()而不是依赖df.plot()。5. ColumnDataSource 实战从“数据容器”到“交互引擎”的深度开发5.1 构建健壮数据源dict初始化的五个黄金法则ColumnDataSource 的data参数看着像普通字典但有严格约束。我总结出初始化时的五条铁律所有值必须是同长度的序列len(data[x]) len(data[y]) len(data[color])。Bokeh 不检查长度但运行时报Length mismatch。禁止嵌套结构data{users: [{name:a}, {name:b}]}会失败。必须展平data{user_name:[a,b]}。日期列必须是datetime64[ns]pd.Timestamp对象可以但datetime.datetime不行需先转np.datetime64。字符串列避免objectdtype用astype(string)替代astype(object)防止 Bokeh 解析失败。数值列显式声明dtypeprice: np.array([1.1,2.2]).astype(float64)避免float32导致精度丢失。安全初始化模板def safe_cds(df): data {} for col in df.columns: s df[col] if pd.api.types.is_datetime64_any_dtype(s): data[col] s.astype(datetime64[ns]).to_numpy() elif pd.api.types.is_string_dtype(s): data[col] s.astype(string).to_numpy() else: # 强制转 float64避免 int32 溢出 data[col] pd.to_numeric(s, downcastfloat).astype(float64).to_numpy() return ColumnDataSource(data) source safe_cds(df_goog)5.2 动态更新策略stream()、patch()与data的适用场景ColumnDataSource 更新有三种方式选错会导致性能灾难source.data new_dict全量替换。适合数据量小1万行或结构变更如增减列。缺点触发全图重绘旧数据状态丢失如用户已选中的点。source.stream(new_data, rollover1000)追加流式数据。rollover控制最大行数超限自动删除最早行。适合实时监控如传感器数据内存恒定。source.patch({column_name: [(row_index, new_value), ...]})局部更新。只改指定位置的值不触发重绘仅更新对应图形元素。适合高频更新如每秒改10个点的颜色。实测性能对比1万行数据方法内存增长CPU 占用是否保留选择状态data8MB12%否stream()0.1MB3%是新增行可选patch()0.01MB1%是工业级建议实时系统用stream()交互式分析用patch()批量导入用data。5.3 跨图表联动用CustomJS实现“点击即联动”的底层逻辑ColumnDataSource 的真正威力在于它能让不同图表“看到彼此”。比如点击左侧散点图的点右侧直方图自动聚焦该点所在区间。这需要CustomJSBokeh 的 JavaScript 回调# 创建两个数据源 scatter_source ColumnDataSource(df_scatter) hist_source ColumnDataSource(df_hist) # 散点图 scatter figure() scatter.circle(x, y, sourcescatter_source, size10) # 直方图 hist figure() hist.quad(top, bottom, left, right, sourcehist_source) # JavaScript 回调当 scatter_source 被选中时... callback CustomJS(argsdict(scatter_sourcescatter_source, hist_sourcehist_source), code // 获取选中的行索引 const indices scatter_source.selected.indices; if (indices.length 0) return; // 计算 x 值范围 const x_data scatter_source.data[x]; const min_x Math.min(...indices.map(i x_data[i])); const max_x Math.max(...indices.map(i x_data[i])); // 更新直方图数据源伪代码实际需计算 bin hist_source.data[top] compute_histogram(x_data, min_x, max_x); hist_source.change.emit(); // 通知 Bokeh 更新 ) scatter_source.selected.js_on_change(indices, callback)这段代码的核心是js_on_change——它把 Python 的事件监听桥接到浏览器端的 JavaScript。CustomJS中可以直接访问source.data执行复杂计算再调用change.emit()触发重绘。这是 Bokeh 区别于其他库的“核武器”。注意CustomJS里不能调用 Python 函数所有逻辑必须用 JS 实现。复杂计算建议提前在 Python 端算好存入source的额外列中JS 只做索引映射。6. 常见问题与排查技巧实录那些文档里不会写的血泪教训6.1 “图出来了但点不动”——交互失效的四大元凶问题现象图表正常显示但鼠标悬停无提示、点击无反应、缩放无效。排查清单检查output_file()和show()顺序必须先output_file(plot.html)再show(plot)。如果漏掉output_file()Bokeh 会尝试用output_notebook()但在某些 Jupyter 环境如 VS Code中失效。确认HoverTool已添加悬停提示不是默认开启的必须显式添加from bokeh.models import HoverTool hover HoverTool(tooltips[(Date, date{%F}), (Price, close{$0,0.00})], formatters{date: datetime}) plot.add_tools(hover)检查source是否被覆盖如果后续代码写了source ColumnDataSource(new_data)旧的source引用就失效了绑定的事件全部丢失。浏览器控制台报错按 F12 打开开发者工具切换到 Console 标签。常见错误Uncaught ReferenceError: Bokeh is not defined表示 Bokeh JS 未加载需检查网络或换用output_file()。6.2 “日期轴乱码”——从2023-01-01到Jan 1的格式化秘籍问题现象x 轴显示2023-01-01 00:00:00而非简洁的Jan 1。解决方案用DatetimeTickFormatter精确控制from bokeh.models import DatetimeTickFormatter plot.xaxis.formatter DatetimeTickFormatter( hours[%d %b], days[%d %b], months[%b %Y], years[%Y] )hours控制小时级刻度days控制天级依此类推。%b是英文缩写Jan%d是日期%Y是四位年份。这样当图表宽度变化时Bokeh 会自动选择最合适的格式。6.3 “内存爆炸”——Jupyter 中反复运行单元格的隐形杀手问题现象第一次运行show(plot)正常第二次运行后浏览器卡死内存占用飙升。根本原因show()每次都创建新文档旧文档未销毁。Jupyter 中尤其严重。解决方法方案1推荐用curdoc().clear()清理当前文档from bokeh.io import curdoc curdoc().clear() # 运行前加这一行 show(plot)方案2改用output_file()输出 HTML 文件避免 Jupyter 渲染器冲突。方案3重启内核治标不治本。6.4 “中文乱码”——字体缺失的终极修复问题现象标题、坐标轴标签显示为方框 □□□。原因Bokeh 默认用 Sans-Serif 字体系统无中文字体时 fallback 失败。三步修复下载思源黑体免费开源https://github.com/adobe-fonts/source-han-sans将SourceHanSansSC-Regular.otf放入项目目录在代码中指定字体路径from bokeh.resources import INLINE from bokeh.util.dependencies import export # 注册字体需在 show() 前 export(SourceHanSansSC, SourceHanSansSC-Regular.otf) plot.title.text_font SourceHanSansSC plot.xaxis.axis_label_text_font SourceHanSansSC6.5 “部署失败”——从本地到服务器的环境鸿沟问题现象本地show(plot)正常部署到服务器后白屏。关键检查点Python 版本Bokeh 3.x 要求 Python 3.8服务器可能是 3.6。依赖版本冲突pip install bokeh可能装错版本。生产环境必须锁定pip install bokeh3.0,3.1缺少nodejsBokeh 服务端渲染bokeh serve需要 nodejs 编译 JS。Ubuntu 上sudo apt install nodejs npm。防火墙bokeh serve默认端口 5006需开放。我踩过的最深的坑某次部署到阿里云 ECS一切配置正确但始终 502 错误。最后发现是安全组规则里只放行了 80/443忘了加 5006。花了6小时查日志教训深刻——部署前先 telnet 测试端口连通性。7. 从入门到落地一个完整的股票分析仪表盘实战7.1 需求拆解业务方真正要什么我们常陷入技术细节忘了业务本质。以股票分析为例业务方要的从来不是“一张漂亮的图”而是快速定位异常哪天价格突变哪个指标偏离均值交叉验证假设成交量放大时价格是否一定上涨分享结论一键导出带标注的图片或生成可交互链接。所以仪表盘必须包含主趋势图带移动平均线成交量柱状图与主图联动相关性散点图支持筛选时间段实时统计面板当前价、涨跌幅、RSI7.2 代码骨架用 ColumnDataSource 统一数据流# 1. 数据加载与预处理一次 df pd.read_csv(stocks.csv) df[date] pd.to_datetime(df[date]).dt.tz_localize(UTC) df[ma20] df[close].rolling(20).mean() df[volume_ma] df[volume].rolling(10).mean() # 2. 构建统一数据源 source ColumnDataSource(df) # 3. 创建主图价格均线 p1 figure(x_axis_typedatetime, width800, height400) p1.line(date, close, sourcesource, colorblue, legend_labelClose) p1.line(date, ma20, sourcesource, colorred, legend_labelMA20) # 4. 创建成交量图共享 x 轴 p2 figure(x_axis_typedatetime, x_rangep1.x_range, width800, height200) p2.vbar(date, 0.5, volume, sourcesource, colorgray, alpha0.5) # 5. 创建散点图支持时间筛选 def make_scatter(start_date, end_date): mask (df[date] start_date) (df[date] end_date) scatter_source ColumnDataSource(df[mask]) p figure(width500, height400) p.circle(volume, close, sourcescatter_source, size5, alpha0.7) return p # 6. 添加交互工具 p1.add_tools(HoverTool(tooltips[(Date, date{%F}), (Close, close{$0,0.00})])) p1.add_tools(BoxSelectTool()) # 框选区域触发散点图更新这个骨架体现了 Bokeh 的核心思想数据是中心视图是投影交互是连接线。所有图表都指向source修改source.data所有图同步更新添加BoxSelectTool用户框选主图区域即可提取对应数据生成新视图。7.3 部署上线bokeh serve的生产级配置本地开发用show()生产必须用bokeh serve。创建app.pyfrom bokeh.io import curdoc from bokeh.layouts import column from bokeh.plotting import figure # 复制上面的图表创建代码 # ... # 将所有图表放入布局 layout column(p1, p2, make_scatter(2023-01-01, 2023-12-31)) curdoc().add_root(layout) curdoc().title Stock Dashboard启动命令bokeh serve app.py --port 8080 --allow-websocket-originlocalhost:8080 --num-procs 4--port指定端口--allow-websocket-origin允许跨域前端页面可嵌入--num-procs多进程提升并发能力访问 http://localhost:8