<audio id="audio" title="46 | 答疑（四）：共享字典的缓存是必须的吗？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/8a/03c78ad7c882bf0982dde3603abc3c8a.mp3"></audio>

你好，我是温铭。

专栏更新到现在，OpenResty第四版块 OpenResty 性能优化篇，我们就已经学完了。恭喜你没有掉队，仍然在积极学习和实践操作，并且热情地留下了你的思考。

很多留言提出的问题很有价值，大部分我都已经在App里回复过，一些手机上不方便回复的或者比较典型、有趣的问题，我专门摘了出来，作为今天的答疑内容，集中回复。另一方面，也是为了保证所有人都不漏掉任何一个重点。

下面我们来看今天的这 5 个问题。

## 问题一：如何完成 Lua 模块的动态加载？

Q：关于OpenResty 实现的动态加载，我有个疑问：在完成新文件替换后，如何用 loadstring 函数完成新文件的加载呢 ？我了解到，loadstring 只能加载字符串，如果要重新加载一个 lua 文件/模块，在 OpenResty 中要如何做到呢？

A：我们知道，loadstring 是加载字符串使用的，而loadfile 可以加载指定的文件，比如： `loadfile("foo.lua")`。事实上，这两个命令达到的效果是一样的。

至于如何加载 Lua 模块，下面是一个具体的示例：

```
resty -e 'local s = [[
local ngx = ngx
local _M = {}
function _M.f()
    ngx.say(&quot;hello world&quot;)
end
return _M
]]
local lua = loadstring(s)
local ret, func = pcall(lua)
func.f()'

```

这里的字符串 `s`，它的内容就是一个完整的 Lua 模块。所以，在发现这个模块的代码有变化时，你可以用 loadstring 或者 loadfile 来重启加载。这样，其中的函数和变量都会随之更新。

更进一步，你也把可以把获取变化和重新加载，用名为 `code_loader` 函数做一层包装：

```
local func = code_loader(name)

```

这样一来，代码更新就会变得更为简洁；同时， `code_loader` 中我们一般会用 lru cache 对 `s` 做一层缓存，避免每一次都去调用 loadstring。这差不多就是一个完整的实现了。

## 问题二：OpenResty 为什么不禁用阻塞操作？

Q：这些年来，我一直有个疑虑，既然这些阻塞调用是官方极力不鼓励的，为什么不直接禁用呢？或者加一个 flag 让用户选择禁用呢？

A：这里说一下我个人的看法。首先是因为 OpenResty 的周边生态还不够完善，有时候我们不得不调用阻塞的库来实现一些功能。比如 ，在1.15.8 版本之前，调用外部的命令行还需要走 Lua 库的 `os.execute`，而不是 `lua-resty-shell`；再如，在 OpenResty 中，读写文件至今还是只能走 Lua 的 I/O 库，并没有非阻塞的方式来替代。

其次，OpenResty 在这种优化上的态度是很谨慎的。比如， `lua-resty-core` 已经开发完成很长时间了，但一直都没有默认开启，需要你手工来调用 `require 'resty.core'`。直到最新的 1.15.8版本，它才得以转正。

最后，OpenResty 的维护者更希望，通过编译器和 DSL自动生成高度优化过的 Lua 代码，这种方式来规范阻塞方式的调用。所以，大家并没有在 OpenResty 平台本身上，去做类似 flag 选项的努力。当然，这种方向是否能够解决实际的问题，我是保留态度的。

站在外部开发者的角度，如何避免这种阻塞，才是更为实际的问题。我们可以扩展 Lua 代码的检测工具，比如 luacheck 等，发现并对常见的阻塞操作进行告警；也可以直接通过改写 `_G` 的方式，来侵入式地禁止或者改写某些函数，比如：

```
resty -e '_G.ngx.print = function()
ngx.say(&quot;hello&quot;)
end
ngx.print()'
hello

```

这样的示例代码，就可以直接改写 `ngx.print` 函数了。

## 问题三：LuaJIT 的 NYI 的操作，是否会对性能有很大影响？

Q：loadstring 在 LuaJIT 的 NYI 列表是 never，会不会对性能有很大影响？

A：关于 LuaJIT 的 NYI，我们不用矫枉过正。对于可以 JIT 的操作，自然是 JIT 的方式最好；但对于还不能 JIT 的操作，我们也不是不能使用。

对于性能优化，我们需要用基于统计的科学方法来看待，这也就是火焰图采样的意义。过早优化是万恶之源。对于那些调用次数频繁、消耗 CPU 很高的热代码，我们才有优化的必要。

回到loadstring 的问题，我们只会在代码发生变化的时候，才会调用它重新加载，和请求多少无关，所以它并不是一个频繁的操作。这个时候，我们就不用担心它对系统整体性能的影响。

结合第二个阻塞的问题，在 OpenResty 中，我们有些时候也会在 init 和 init worker 阶段，去调用阻塞的文件 I/O 操作。这种操作比 NYI 更加影响性能，但因为它只在服务启动的时候执行一次，所以也是可以被我们接受的。

还是那句话，性能优化要从宏观的视角来看待，这是你特别需要注意的一个点。否则，纠结于某一细节，就很有可能优化了半天，却并没有起到很好的效果。

## 问题四：动态上游可以自己来实现吗？

Q：动态上游这块，我的做法是为一个服务设置 2 个 upstream，然后根据路由条件选择不同的 upstream，当机器 IP 有变化时，直接修改 upstream 中的 IP 即可。这样的做法，和直接使用 `balancer_by_lua` 相比，有什么劣势或坑吗？

A：单独看这个案例。`balancer_by_lua` 的优势，是可以让用户选择负载均衡的算法，比如是用roundrobin 还是 chash，又或者是用户自己实现的其他算法都可以，灵活而且性能很高。

如果按照路由规则的方式来做，从最终结果上来看是一样的。但上游健康检查需要你自己来实现，增加了不少额外的工作量。

我们也可以扩展下这个提问，对于 abtest 这种需要不同上游的场景，我们应该如何去实现呢？

你可以在 `balancer_by_lua` 阶段中，根据 uri、host、参数等来决定使用哪一个上游。你也可以使用 API 网关，把这些判断变为路由的规则，在最开始的 `access` 阶段，通过判断决定使用哪一个路由，再通过路由和上游的绑定关系找到指定的上游。这就是 API 网关的常见做法，后面在实战章节中，我们会更具体地聊到。

## 问题五：共享字典的缓存是必须的吗？

Q：在实际的生产应用中，我认为 shared dict 这一层缓存是必须的。貌似大家都只记得 lruca  che 的好，数据格式没限制、不需要反序列化、不需要根据 k/v 体积算内存空间、worker 间独立不相互争抢、没有读写锁、性能高云云。

但是，却忘记了它最致命的一个弱点，就是 lru  cache 的生命周期是跟着 worker 走的。每当Nginx reload 时，这部分缓存会全部丢失，这时候，如果没有 shared dict，那 L3 的数据源分分钟被打挂。

当然，这是并发比较高的情况下，但是既然用到了缓存，就说明业务体量肯定不会小，也就是刚刚的分析仍然适用。不知道我的这个观点对吗？

A：大部分情况下，确实如你所说，共享字典在 reload 的时候不会丢失，所以它有存在的必要性。但也有一种特例，那就是，如果在 `init` 阶段或者 `init_worker` 阶段，就能从 L3 也就是数据源主动获取到所有数据，那么只有 lru cache 也是可以接受的。

举例来说，比如开源 API 网关 [APISIX](https://github.com/iresty/apisix) 的数据源在 etcd 中，它只在 `init_worker` 阶段，从 etcd 中获取数据并缓存在lru cache 中，后面的缓存更新，都是通过 etcd 的 watch 机制来主动获取的。这样一来，即使 Nginx reload ，也不会有缓存风暴产生。

所以，对待技术的选择，我们可以有倾向，但还是不要一概而论绝对化，因为并没有一个可以适合所有缓存场景的银弹。根据实际场景的需要，构建一个最小化可用的方案，然后逐步地增加，是一个不错的法子。

今天主要解答这几个问题。最后，欢迎你继续在留言区写下你的疑问，我会持续不断地解答。希望可以通过交流和答疑，帮你把所学转化为所得。也欢迎你把这篇文章转发出去，我们一起交流、一起进步。


