关于lua:如何将一个-Lua-Table-序列化

 有时因为业务须要,要将一个 Lua Table 序列化,如果不在乎序列化后果的可读性,能够间接将 Table 序列化成二进制数据块,如果须要后果的可读性,能够将 Table 序列化成字符串。 要序列化的内容 因为 Lua 的 Table 中能够保留各种各样的内容,不可能都序列化,而且根本也不会有这种需要,比方像是 Function 这种变量,序列化进去并无意义,因为在另一个环境下反序列化当前就不能用了。 出于这个起因, Function 和 Thread 变量不论是 key 还是 value 都不会解决。如果 key 是 Table 变量,则也不解决,同时 Table 中不能有对其它 Table 的循环援用。 尽管看起来有一些限度,然而因为序列化 Table 个别都是用在保留或者发送上,所以其实自身就基本上不会有这种受限制的需要。 二进制 将 Table 序列化为二进制数据块的速度是最快的,体积也是最小的,如果没有可读性要求,且序列化远比反序列化的次数多,那就果决抉择序列化成二进制。 云风大佬有一个现成的实现能够间接拿来用 lua-serialize,本人实现一个也不难,就是应用 Lua 提供的 C Api 遍历 Table,而后依据 key 和 value 的类型,一直写入到一个能够动静扩容的内存块链表中,全副写入实现当前再将链表中的所有块拷贝进一个间断内存中。 除了后果没有可读性以外,序列化成二进制还有一个毛病就是后果要反序列化成 Table 的时候仍然须要逆向解析一遍。 字符串 序列化成字符串的长处是,后果可读性好,而且不须要额定实现反序列化,能够间接应用 load 来加载字符串实现反序列化。 Lua 实现 用 Lua 来实现是最顺畅最简略的,不必写 C 代码,也不须要编译。如果没有高频应用的话,用 Lua 序列化就足够用了。 有一个十分好的第三方实现能够用来解决这个问题,serpent,功能强大,不仅能够序列化,还能够将 Table 以可读形式打印进去。 ...

August 30, 2023 · 1 min · jiezi

关于lua:构建Lua解释器虚拟机的基础

前言在本篇,咱们正式进入到Lua解释器的开发阶段(这是一个遵循Lua 5.3规范的我的项目)。本篇并不间接接入到设计和实现语法分析器和词法分析器的阶段,而是先设计和实现Lua虚拟机的根底数据结构(包含Lua最根本的数据结构,如根本数据类型、示意虚拟机状态的global_State和lua_State构造、在函数调用中表演重要角色的CallInfo构造等)以及设计和实现基于栈的C函数调用流程。这些都是了解前面虚拟机运作的根底。因为这是一个仿造我的项目,为了和官网版本做辨别,就称之为dummylua,前面要称说本我的项目时,一律用dummylua来示意。 本篇将分为几个局部:首先介绍工程目录构造的组织,以及为什么要这样组织,每个目录别离蕴含哪些文件,这些文件别离蕴含哪些内容;其次是着手进行Lua根本类型的设计与实现,实现示意Lua虚拟机状态的global_State和lua_State构造,以及用于函数调用的CallInfo构造;接着是设计咱们在应用层,要实现C函数,在栈中调用流程的API,并且构建从虚拟机状态初始化到函数实现调用的逻辑图,论述这个过程;最初通过编写代码,将所有的流程实现。第一局部的性能曾经实现开发和测试(Ubuntu、Mac和Windows平台)。 获取源码能够查看:https://github.com/Manistein/... 目录构造在开始介绍我的项目的目录构造之前,咱们无妨先回顾一下Lua运作的两种基本模式。 一种是创立Lua虚拟机,间接加载脚本并且间接运行,其遵循如下流程: 创立Lua虚拟机状态实例加载规范库加载脚本,通过词法分析器Lexer和语法分析器Parser将脚本编译成Lua虚拟机可能辨认的Opcodes,并存储在虚拟机状态实例中运行虚拟机,对虚拟机状态实例中的Opcodes进行执行还有一种则是,事后将脚本编译,而后将内存中的指令信息,Dump到文件中,以Bytecode的模式存在,当前要运行的时候,间接加载Dump文件的Bytecode并且间接运行: 创立Lua虚拟机状态实例加载规范库加载脚本,通过词法分析器Lexer和语法分析器Parser将脚本编译成Lua虚拟机可能辨认的Opcodes,并存储在虚拟机状态实例中将虚拟机指令Dump成二进制文件,以Bytecode的模式保留在未来某个时刻,运行Lua虚拟机,并加载Dump后的文件,间接通过Dump数据,将指令构造还原回来,保留在虚拟机状态实例中运行虚拟机,对虚拟机状态实例中的Opcodes进行执行这两种形式,前者从虚拟机创立到加载脚本,再到运行零打碎敲。后者须要事后将Lua脚本编译成Bytecode,而后要应用的时候再加载运行,运行时省去了编译流程,比前者更快。不过当初Lua间接加载脚本并运行曾经足够快了,除非对性能有极其刻薄的要求,否则前者曾经可能满足咱们的日常须要了。上面援用一张《The Lua Architecture》(Reference 1)中的一张图,来展现一下前面一种形式的流程。 从上图,咱们能够理解到一个流程,就是咱们要运行Lua脚本,首先要创立Lua解释器(因为Lua是采纳纯C来写的工程,因而函数和数据是拆散的,这里其实也只是创立一个Lua虚拟机状态实例,也就是前面咱们要介绍的lua_State构造和global_State构造),而后通过编译器(Lexer和Parser)将脚本编译成虚拟机可能辨认的指令(Opcodes),再交给虚拟机执行。因而,咱们能够将编译和运行宰割开来,他们独特应用的局部也独自抽离进去,于是咱们的目录构造能够按如下所示的形式组织: + 3rd/ #援用的第三方库均搁置在这里+ bin/ #编译生成的二进制文件搁置在这里+ clib/ #内部要在c层应用Lua的C API,那么只能调用clib里提供的接口,而不能调用其余外部接口+ common/ #vm和compiler独特应用的构造、接口均搁置在这里+ compiler/ #编译器相干的局部搁置在这里+ test/ #测试用例全副搁置在这里+ vm/ #虚拟机相干的局部搁置在这里 main.c makefile咱们有理由置信,目录组织也是架构的一部分,下面附上了目录阐明,可能清晰阐明他们的分类和作用。我想构建的逻辑档次图如下所示: 3rd和common作为vm和compiler的根底模块而存在,内部在应用c接口的时候,只能通过clib里的辅助库来进行,以暗藏不必要裸露的细节。在定下目录构造当前,接下来将展现不同的文件别离有哪些文件。 目录组织实现当前,接下来确定有哪些文件了,我将文件内容展现到上面局部: + 3rd/ #援用的第三方库均搁置在这里+ bin/ #编译生成的二进制文件搁置在这里~ clib/ #内部要在c层应用Lua的C API,那么只能调用clib里提供的接口,而不能调用其余外部接口 luaaux.h #供内部应用的辅助库 luaaux.c~ common/ #vm和compiler独特应用的构造、接口均搁置在这里 lua.h #提供lua根本类型的定义,错误码定义,全我的项目都可能用到的宏均会搁置在这里 luamem.h #lua内存分配器 luamem.c luaobject.h #lua根本类型 luaobject.c luastate.h #虚拟机状态构造,以及对其相干操作的接口均搁置于此 luastate.c+ compiler/ #编译器相干的局部搁置在这里+ test/ #测试用例全副搁置在这里~ vm/ #虚拟机相干的局部搁置在这里 luado.h #函数调用相干的接口均搁置于此 luado.c main.c makefile下面展现了咱们本局部要实现的局部,后续开发会陆续增加新的文件,前面章节也会陆续援用这个片段。到当初为止,咱们的目录构造就介绍完了,前面将介绍根本数据结构。 根本数据结构根本类型Lua的根本类型,包含lua_Integer、lua_Number、lu_byte、lua_CFunction等,当然最典型的则是其可能代表任何根本类型的TValue构造。当初咱们将逐个实现这些类型。 首先咱们要实现两个宏,LUA_INTEGER和LUA_NUMBER在common/lua.h里: ...

February 9, 2023 · 11 min · jiezi

关于lua:Y-分钟速成-Lua

源代码下载: lua-cn.lua -- 单行正文以两个连字符结尾 --[[ 多行正文--]]---------------------------------------------------- -- 1. 变量和流程管制---------------------------------------------------- num = 42 -- 所有的数字都是双精度浮点型。-- 别胆怯,64位的双精度浮点型数字中有52位用于 -- 保留准确的整型值; 对于52位以内的整型值, -- 不必放心精度问题。s = 'walternate' -- 和Python一样,字符串不可变。 t = "也能够用双引号" u = [[ 多行的字符串 以两个方括号 开始和结尾。]] t = nil -- 撤销t的定义; Lua 反对垃圾回收。 -- 块应用do/end之类的关键字标识: while num < 50 do num = num + 1 -- 不反对 ++ 或 += 运算符。 end -- If语句: if num > 40 then print('over 40') elseif s ~= 'walternate' then -- ~= 示意不等于。 -- 像Python一样,用 == 查看是否相等 ;字符串同样实用。 io.write('not over 40\n') -- 默认规范输入。else -- 默认全局变量。 thisIsGlobal = 5 -- 通常应用驼峰。 -- 如何定义局部变量: local line = io.read() -- 读取规范输出的下一行。 -- ..操作符用于连贯字符串: print('Winter is coming, ' .. line) end -- 未定义的变量返回nil。 -- 这不是谬误: foo = anUnknownVariable -- 当初 foo = nil. aBoolValue = false --只有nil和false为假; 0和 ''均为真! if not aBoolValue then print('false') end -- 'or'和 'and'短路 -- 相似于C/js里的 a?b:c 操作符: ans = aBoolValue and 'yes' or 'no' --> 'no' karlSum = 0 for i = 1, 100 do -- 范畴蕴含两端 karlSum = karlSum + i end -- 应用 "100, 1, -1" 示意递加的范畴: fredSum = 0 for j = 100, 1, -1 do fredSum = fredSum + j end -- 通常,范畴表达式为begin, end[, step]. -- 循环的另一种构造: repeat print('the way of the future') num = num - 1 until num == 0 ---------------------------------------------------- -- 2. 函数。 ---------------------------------------------------- function fib(n) if n < 2 then return n end return fib(n - 2) + fib(n - 1)end-- 反对闭包及匿名函数: function adder(x) -- 调用adder时,会创立返回的函数, -- 并且会记住x的值: return function (y) return x + y end end a1 = adder(9) a2 = adder(36) print(a1(16)) --> 25 print(a2(64)) --> 100 -- 返回值、函数调用和赋值都能够-- 应用长度不匹配的list。 -- 不匹配的接管方会被赋值nil; -- 不匹配的发送方会被抛弃。 x, y, z = 1, 2, 3, 4 -- x = 1、y = 2、z = 3, 而 4 会被抛弃。 function bar(a, b, c) print(a, b, c) return 4, 8, 15, 16, 23, 42 end x, y = bar('zaphod') --> 打印 "zaphod nil nil" -- 当初 x = 4, y = 8, 而值15..42被抛弃。 -- 函数是一等公民,能够是部分的,也能够是全局的。 -- 以下表达式等价: function f(x) return x * x end f = function (x) return x * x end -- 这些也是等价的: local function g(x) return math.sin(x) endlocal g; g = function (x) return math.sin(x) end-- 以上均因'local g',使得g能够自援用。local g = function(x) return math.sin(x) end-- 等价于 local function g(x)..., 但函数体中g不可自援用-- 顺便提下,三角函数以弧度为单位。 -- 用一个字符串参数调用函数,能够省略括号: print 'hello' --能够工作。 -- 调用函数时,如果只有一个table参数,-- 同样能够省略括号(table详情见下):print {} -- 一样能够工作。---------------------------------------------------- -- 3. Table。 ---------------------------------------------------- -- Table = Lua惟一的组合数据结构; -- 它们是关联数组。 -- 相似于PHP的数组或者js的对象, -- 它们是哈希表或者字典,也能够当列表应用。 -- 按字典/map的形式应用Table: -- Dict字面量默认应用字符串类型的key: t = {key1 = 'value1', key2 = false} -- 字符串key能够应用相似js的点标记: print(t.key1) -- 打印 'value1'. t.newKey = {} -- 增加新的键值对。 t.key2 = nil -- 从table删除 key2。 -- 应用任何非nil的值作为key: u = {['@!#'] = 'qbert', [{}] = 1729, [6.28] = 'tau'} print(u[6.28]) -- 打印 "tau" -- 数字和字符串的key按值匹配的-- table按id匹配。 a = u['@!#'] -- 当初 a = 'qbert'. b = u[{}] -- 咱们或者期待的是 1729, 然而失去的是nil: -- b = nil ,因为没有找到。 -- 之所以没找到,是因为咱们用的key与保留数据时用的不是同-- 一个对象。 -- 所以字符串和数字是移植性更好的key。 -- 只须要一个table参数的函数调用不须要括号: function h(x) print(x.key1) end h{key1 = 'Sonmi~451'} -- 打印'Sonmi~451'. for key, val in pairs(u) do -- 遍历Table print(key, val) end -- _G 是一个非凡的table,用于保留所有的全局变量 print(_G['_G'] == _G) -- 打印'true'. -- 按列表/数组的形式应用: -- 列表字面量隐式增加整数键: v = {'value1', 'value2', 1.21, 'gigawatts'} for i = 1, #v do -- #v 是列表的大小 print(v[i]) -- 索引从 1 开始!! 太疯狂了! end-- 'list'并非真正的类型,v 其实是一个table, -- 只不过它用间断的整数作为key,能够像list那样去应用。 ---------------------------------------------------- -- 3.1 元表(metatable) 和元办法(metamethod)。 ---------------------------------------------------- -- table的元表提供了一种机制,反对相似操作符重载的行为。-- 稍后咱们会看到元表如何反对相似js prototype的行为。 f1 = {a = 1, b = 2} -- 示意一个分数 a/b. f2 = {a = 2, b = 3} -- 这会失败:-- s = f1 + f2 metafraction = {} function metafraction.__add(f1, f2) local sum = {} sum.b = f1.b * f2.b sum.a = f1.a * f2.b + f2.a * f1.b return sumendsetmetatable(f1, metafraction) setmetatable(f2, metafraction) s = f1 + f2 -- 调用在f1的元表上的__add(f1, f2) 办法 -- f1, f2 没有对于元表的key,这点和js的prototype不一样。 -- 因而你必须用getmetatable(f1)获取元表。-- 元表是一个一般的table, -- 元表的key是一般的Lua中的key,例如__add。 -- 然而上面一行代码会失败,因为s没有元表: -- t = s + s -- 上面提供的与类类似的模式能够解决这个问题: -- 元表的__index 能够重载用于查找的点操作符: defaultFavs = {animal = 'gru', food = 'donuts'} myFavs = {food = 'pizza'} setmetatable(myFavs, {__index = defaultFavs}) eatenBy = myFavs.animal -- 能够工作!感激元表 -- 如果在table中间接查找key失败,会应用-- 元表的__index 递归地重试。-- __index的值也能够是function(tbl, key)-- 这样能够反对自定义查找。 -- __index、__add等的值,被称为元办法。 -- 这里是一个table元办法的清单: -- __add(a, b) for a + b -- __sub(a, b) for a - b -- __mul(a, b) for a * b -- __div(a, b) for a / b -- __mod(a, b) for a % b -- __pow(a, b) for a ^ b -- __unm(a) for -a -- __concat(a, b) for a .. b -- __len(a) for #a -- __eq(a, b) for a == b -- __lt(a, b) for a < b -- __le(a, b) for a <= b -- __index(a, b) <fn or a table> for a.b -- __newindex(a, b, c) for a.b = c -- __call(a, ...) for a(...) ---------------------------------------------------- -- 3.2 与类类似的table和继承。 ---------------------------------------------------- -- Lua没有内建的类;能够通过不同的办法,利用表和元表-- 来实现类。 -- 上面是一个例子,解释在前面: Dog = {} -- 1. function Dog:new() -- 2. local newObj = {sound = 'woof'} -- 3. self.__index = self -- 4. return setmetatable(newObj, self) -- 5. end function Dog:makeSound() -- 6. print('I say ' .. self.sound) end mrDog = Dog:new() -- 7. mrDog:makeSound() -- 'I say woof' -- 8. -- 1. Dog看上去像一个类;其实它是一个table。 -- 2. 函数tablename:fn(...) 等价于-- 函数tablename.fn(self, ...)-- 冒号(:)只是增加了self作为第一个参数。 -- 浏览7 & 8条 理解self变量是如何失去其值的。 -- 3. newObj是类Dog的一个实例。 -- 4. self = 被继承的类。通常self = Dog,不过继承能够扭转它。 -- 如果把newObj的元表和__index都设置为self, -- newObj就能够失去self的函数。 -- 5. 备忘:setmetatable返回其第一个参数。 -- 6. 冒号(:)的作用和第2条一样,不过这里 -- self是一个实例,而不是类 -- 7. 等价于Dog.new(Dog),所以在new()中,self = Dog。 -- 8. 等价于mrDog.makeSound(mrDog); self = mrDog。 ---------------------------------------------------- -- 继承的例子: LoudDog = Dog:new() -- 1. function LoudDog:makeSound() local s = self.sound .. ' ' -- 2. print(s .. s .. s) end seymour = LoudDog:new() -- 3. seymour:makeSound() -- 'woof woof woof' -- 4. -- 1. LoudDog取得Dog的办法和变量列表。 -- 2. 因为new()的缘故,self领有了一个'sound' key,参见第3条。 -- 3. 等价于LoudDog.new(LoudDog),转换一下就是 -- Dog.new(LoudDog),这是因为LoudDog没有'new' key, -- 然而它的元表中有 __index = Dog。 -- 后果: seymour的元表是LoudDog,并且 -- LoudDog.__index = Dog。所以有seymour.key -- = seymour.key, LoudDog.key, Dog.key -- 从其中第一个有指定key的table获取。 -- 4. 在LoudDog能够找到'makeSound'的key; -- 等价于LoudDog.makeSound(seymour)。 -- 如果有必要,子类也能够有new(),与基类类似: function LoudDog:new() local newObj = {} -- 初始化newObj self.__index = self return setmetatable(newObj, self) end ---------------------------------------------------- -- 4. 模块 ---------------------------------------------------- --[[ 我把这部分给正文了,这样脚本剩下的局部能够运行 -- 假如文件mod.lua的内容相似这样: local M = {} local function sayMyName() print('Hrunkner') end function M.sayHello() print('Why hello there') sayMyName() end return M -- 另一个文件能够应用mod.lua的性能: local mod = require('mod') -- 运行文件mod.lua. -- require是蕴含模块的规范做法。 -- require等价于: (针对没有被缓存的状况;参见前面的内容) local mod = (function () <contents of mod.lua> end)() -- mod.lua被包在一个函数体中,因而mod.lua的局部变量-- 对外不可见。 -- 上面的代码能够工作,因为在这里mod = mod.lua 中的 M: mod.sayHello() -- Says hello to Hrunkner. -- 这是谬误的;sayMyName只在mod.lua中存在: mod.sayMyName() -- 谬误 -- require返回的值会被缓存,所以一个文件只会被运行一次, -- 即便它被require了屡次。 -- 假如mod2.lua蕴含代码"print('Hi!')"。 local a = require('mod2') -- 打印Hi! local b = require('mod2') -- 不再打印; a=b. -- dofile与require相似,然而不缓存: dofile('mod2') --> Hi! dofile('mod2') --> Hi! (再次运行,与require不同) -- loadfile加载一个lua文件,然而并不运行它。 f = loadfile('mod2') -- Calling f() runs mod2.lua. -- loadstring是loadfile的字符串版本。 g = loadstring('print(343)') --返回一个函数。 g() -- 打印343; 在此之前什么也不打印。 --]] 参考为什么?我十分兴奋地学习lua, 这样我就能够应用 Löve 2D 游戏引擎来编游戏。 ...

December 3, 2022 · 6 min · jiezi

关于lua:Unity移动端游戏性能优化简谱之-常见游戏内存控制

《Unity挪动端游戏性能优化简谱》从Unity挪动端游戏优化的一些根底探讨登程,例举和剖析了近几年基于Unity开发的挪动端游戏我的项目中最为常见的局部性能问题,并展现了如何应用UWA的性能检测工具确定和解决这些问题。内容包含了性能优化的根本逻辑、UWA性能检测工具和常见性能问题,心愿能提供给Unity开发者更多高效的研发办法和实战经验。 明天向大家介绍文章第二局部:资源内存、Mono堆内存等常见游戏内存管制,共13大节,蕴含了纹理资源、网格资源、动画资源、音频资源、材质资源等多个资源内存以及Mono堆内存等常见的游戏内存管制解说。(全文长约11400字,预计浏览工夫约20分钟) 文章第一局部《Unity挪动端游戏性能优化简谱之 前言》可戳此回顾,残缺内容可返回UWA学堂查看。 1. 总览1.1 概念解释首先,在探讨内存相干的各项参数和制订规范之前,咱们须要先理清在各种性能工具的统计数据中常呈现的各种内存参数的理论含意。 在安卓零碎中,咱们最常见到和关怀的PSS(Proportional Set Size)内存,其含意为一个过程在RAM中理论应用的空间地址大小,即理论应用的物理内存。就后果而言,当一个游戏过程中PSS内存峰值越高、占以后硬件的总物理内存的比例越高,则该游戏过程被零碎杀死(闪退)的概率也就越高。 而在PSS内存中,除了Unused局部外,咱们个别比较关心Reserved Total内存和Lua、Native代码、插件等零碎缓存、第三方库的本身调配等内存。Reserved Total占比个别较高,故其大小和走势,也是UWA性能剖析工具的次要统计对象(对于应用到Lua的我的项目,UWA另外提供了Lua专项测试报告统计Lua内存,下文还会提到)。 Reserved Total和Used Total为Unity引擎在内存方面的总体调配量和总体使用量。 一般来说,引擎在分配内存时并不是向操作系统 “即拿即用”,而是首先获取一定量的间断内存,而后供本人外部应用,待空余内存不够时,引擎才会向零碎再次申请一定量的间断内存进行应用。 留神:对于绝大多数平台而言,Reserved Total内存 = Reserved Unity内存 + GFX内存 + FMOD内存 + Mono内存 (1) Reserved Unity内存Reserved Unity和Used Unity为Unity引擎本身各个模块外部的内存调配,包含各个Manager的内存占用、序列化信息的内存占用和局部资源的内存占用等等。 通过针对大量我的项目的深度剖析,UWA发现导致Reserved Unity内存调配较大的起因次要有以下几种: 序列化信息内存占用:Unity引擎的序列化信息品种繁多,其中最为常见且内存占用较大的为SerializedFile。该序列化信息的内存调配次要是我的项目通过特定API(WWW.LoadFromCacheOrDownload、CreateFromFile等)加载AssetBundle文件所致。 资源内存占用:次要包含Mesh、AnimationClip、RenderTexture等资源。对于未开启“Read/Write Enable” 选项的Mesh资源,其内存占用是统计在GFX内存中供GPU应用的,但开启该选项后,网格数据会在Reserved Unity中保留一份,便于我的项目在运行时对Mesh数据进行实时的编辑和批改。同时,如果研发团队同样开启了纹理资源的 “Read/Write Enable” 选项(默认状况下为敞开),则纹理资源同样会在Reserved Unity中保留一份,进而造成其更大的内存占用。 (2) GFX内存 GFX内存为底层显卡驱动所反馈的内存调配量,该内存调配由底层显卡驱动所管制。一般来说,该局部内存占用次要由渲染相干的资源量所决定,包含纹理资源、Mesh资源、Shader资源传向GPU的局部,以及解析这些资源的相干库所调配的内存等。 (3) 托管堆内存托管堆内存示意我的项目运行时代码调配的托管堆内存调配量。对于应用Mono进行代码编译的我的项目,其托管堆内存次要由Mono调配和治理;对于应用IL2CPP进行代码编译的我的项目,其托管堆内存次要由Unity本身调配和治理。 1.2 内存参数规范在咱们理解了内存相干的各项参数的含意之后,晓得了防止游戏闪退的重点在于管制PSS内存峰值。而PSS内存的大头又在于Reserved Total中的资源内存和Mono堆内存。对于应用Lua的我的项目来说,还应关注Lua内存。 依据UWA的教训,只有当PSS内存峰值管制在硬件总内存的0.5-0.6倍以下的时候,闪退危险才较低。举例而言,对于2GB的设施而言,PSS内存应管制在1GB以下为最佳,3GB的设施则应管制在1.5GB以下。 而对于大多数我的项目而言,PSS内存大概高于Reserved Total 200MB-300MB左右,故2GB设施的Reserved Total应管制在700MB以下、3GB设施则管制在1GB以下。 特地的,UWA还认为Mono堆内存须要予以关注,因为在很多我的项目中,Mono堆内存除了存在自身驻留偏高或存在泄露危险的问题外,其大小还会影响GC耗时。UWA认为管制在80MB以下为最佳。 下表为UWA提供的细化到每一种资源内存的举荐规范,制订较为严格。不过,仍须要开发者依据本身我的项目的理论状况予以调整。比方某个2D我的项目节俭了简直所有网格资源的应用,那么其余资源的规范就能够放宽很多。 对于更多的细化规范,大家能够间接在UWA线上产品中进行对应查看。 基于我的项目实情制订内存规范后,个别需进一步与美术、策动协商,给出正当的美术标准参数,并撰写成文档。 定好标准后,定时查看我的项目里的所有美术资源是否符合规范,及时批改和更新。查看美术是否合规的过程,能够利用Unity提供的回调函数写自动化工具,提高效率。能够参考《自动化标准Unity资源的实际》。 如果资源若不能批量解决成高中低配版本,就须要美术为各个画质等级制作不同的资源。 1.3 本地资源检测服务-我的项目资源检测各项资源内存的引擎设置项繁琐且并不都能在运行时被采集,下文行将提到的内容尽管是泛滥我的项目中常见且重要的问题,但理论我的项目中的状况更加简单。通过本地资源检测服务的我的项目资源检测界面,往往能发现更多资源设置项的问题。它们不光影响相干资源的内存占用,还会依据状况对CPU耗时和GPU造成不同水平的压力。 为此UWA依据教训设计了检测规定和阈值,以此为根据采集和统计了存在这些问题的资源,并给出了对应的优化倡议,帮忙开发者针对资源进行更加深刻的排查和优化。 2. 常见的共通性问题这一部分提到的问题没有特定性,不仅仅呈现在一种资源内存中。所以,为了防止赘述,此处对立予以探讨。 ...

June 17, 2022 · 2 min · jiezi

关于lua:ToLua或XLua中的虚拟机是否独立于Unity的主线程

1)ToLua或XLua中的虚拟机是否独立于Unity的主线程2)Timeline技能编辑器提取关键帧信息3)Canvas AdditionalShaderChannels设置有效4)SDK返回的时候取Time.deltaTime比拟大 这是第291篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) LuaQ:ToLua或XLua中的虚拟机是否独立于Unity的主线程? A:Lua的虚拟机有本人的堆、栈内存,虚拟机的更新须要有内部的线程通过函数调用来驱动,而Lua虚拟机目前只反对单个线程来驱动。在理论我的项目中,因为Lua中要拜访Unity的API,所以基本上都是通过主线程来驱动。 但实际上,如果不须要拜访Unity API,齐全是能够放在子线程里去驱动的。 该答复由UWA提供 TimelineQ:最近在用Unity做一个技能编辑器,让策动能够在Timeline编辑器里编辑技能动画、施法特效、受击动画、受击特效和播放声音等等。编辑这些都没什么问题,我想依据编辑好的Timeline导出一系列行为节点,在Editor的环境下导出成配置(蕴含所有行为节点)。当初我不分明怎么获取Timeline里第m帧开始某动画x,第n帧完结这些行为。同时怎么能给动画打上对应Tag? A1:Timeline的组成如下图: 每个Track对应一个TrackAsset其实就是一个PlayableAsset,整个Timeline也是一个TimelineAsset,外面存了所有Track的数据。 对于每个Clip的开始和完结工夫在TimelineClip中能够获取。 感激羽飞@UWA问答社区提供了答复 A2:我把基于Timeline的编辑器小Demo共享上来。 技能编辑器Demo。 以上是自己摸索着写的一个简略技能编辑器Demo,基于Timeline的。 感激题主右前锋@UWA问答社区提供了答复 UGUIQ:在UI中应用到的Shader中用到了UV1和UV2通道,在Canvas AdditionalShaderChannels中抉择增加了对应通道后没有成果,雷同代码资源在2019.4.26f1c1正确显示,求教大家是否是版本问题还是须要非凡设置解决? Unity版本:2018.4.17f1 Unity版本:2019.4.26f1c1 A:搜寻了一下Unity Release Note,在Unity 2019.2.0版本中发现Unity修复了这个问题。修复后,AddVert会把UV2的数据和UV3的数据也拷贝进去。没修复前,UV2的数据从RenderDoc外面看是0。 测试了一下,的确在2019.1.14版本还是有问题的,下一个版本2019.2.0就没问题了。 在2018.4.23f1版本上参考这个链接的Outline成果进行了测试,在小米9真机上没有发现问题,题主能够参考一下:https://www.1024sou.com/artic... PS:看了2018.4.18到2018.4.23之间的所有的Release Note,没有发现相干问题的修复。这个文章是应用Shader来实现Outline的,感觉成果挺不错。 感激Xuan@UWA问答社区提供了答复 ScriptQ:nity调用某SDK进行登录,SDK返回的时候取Time.deltaTime比拟大(就是从调用SDK开始到SDK返回的工夫),这种状况可能导致我在Update里的逻辑经验了很长的一帧。 A1:间接点就是判断Time.deltaTime大于某个阈值时间接Return。感激碎心客@UWA问答社区提供了答复 A2:Time.deltaTime比拟大也就是说登录操作的耗时很高,而且是一个同步调用,的确会导致卡顿问题(在Update里的逻辑经验了很长的一帧)。<br/>一般来说,网络申请等操作应该尽可能通过异步调用来实现,如果是SDK的登录接口没有提供异步办法,倡议先尝试通过子线程(new Thread)来调用,而后主线程上开个协程每帧轮询下后果。该答复由UWA提供 封面图来源于网络 明天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题兴许都只是冰山一角,咱们早已在UWA问答网站上筹备了更多的技术话题等你一起来摸索和分享。欢送酷爱提高的你退出,兴许你的办法恰能解他人的当务之急;而他山之“石”,也能攻你之“玉”。 官网:www.uwa4d.com官网技术博客:blog.uwa4d.com官网问答社区:answer.uwa4d.comUWA学堂:edu.uwa4d.com官网技术QQ群:793972859(原群已满员)

March 28, 2022 · 1 min · jiezi

关于lua:APISIX单元测试准备工作与问题解决方案

最近电脑更新,换了M1的,所有工作从新开始,意味着APISIX插件开发之路中的绊脚石——单测环境筹备,也要从新筹备。 因为降级了芯片和最新的macOS零碎(12.2.1),导致过程并没有设想中的顺利,之前没遇到过的坑,一个个的裸露进去了,这里做一个分享和BackUp。 还有想吐槽一下APISIX官网的文档,在对于单元测试和插件编写方面的内容,输入不是特地的多,而且散落各处,着实给开发人员带来了麻烦。 以下的操作都是基于APISIX源码下操作(https://github.com/apache/apisix),写该文时版本为V2.12.1。失常装置流程装置各种环境依赖 # install OpenResty, etcd and some compilation tools brew install openresty/brew/openresty luarocks lua@5.1 etcd curl git pcre openldap cpanminus # start etcd server brew services start etcd官网最新源码中提供了脚本,位于utils/install-dependencies.sh。间接执行这个脚本就能够对上方的依赖进行装置和启动。 官网文档 批改环境变量结合实际装置状况批改.bash_profile文件,并别忘了 source ~/.bash_profile。 OPENRESTY_HOME=/usr/local/openrestyPATH=$OPENRESTY_HOME/nginx/sbin:$OPENRESTY_HOME/bin:$PATHexport OPENRESTY_HOME下载APISIX源码git clone https://github.com/apache/apisix.git源码环境下解决# 源码根目录下git clone https://github.com/iresty/test-nginx.gitrm -rf test-nginx/.gitsudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)export PERL5LIB=.:$PERL5LIBmake deps我看官网举荐应用 make deps ENV_LUAROCKS_SERVER=https://luarocks.cn,但我试了,并没有起效,仍旧应用 https://luarocks.org,起初我通过批改luarocks配置文件 ~/.luarocks/config-5.1.lua ,失效了,大家也能够本人试试,在文章下方做个反馈。 APISIX文档 rocks_servers = { "https://luarocks.cn"}因为github的网络起因,make deps过程可能不是一帆风顺,有点急躁多试几次就能够了。 ...

March 9, 2022 · 2 min · jiezi

关于lua:AssetBundle异步加载被中断的问题

1)AssetBundle异步加载被中断的问题2)LuaDLL.lua_pcall()本身产生开销问题3)法线在手机渲染时呈现的谬误问题4)UNITY_MATRIX_I_V 和Camera.main.worldToCameraMatrix.inverse区别 这是第272篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) ResourcesQ:在应用异步接口yield return AssetBundle.ASyncLoad的时候,难免会想到:这个异步解决完之前如何Cancel掉这个工作?也就是说一个AssetBundle加载到一半,当初要放弃加载,应该怎么解决? A1:其实底层调用GetAssetBundleBlocking,会在这个函数外部调用PreloadManager的加载更新逻辑,其实就是把Async操作改成了Sync了,会始终阻塞到AssetBundle加载胜利。 然而也有坑,这儿因为执行了PreloadManager.UpdatePreloading,外部还会做GC。遇到过把不应该卸载的资源给卸载了,说是Unused,然而理论正在应用。 感激黄程@UWA问答社区提供了答复 A2:资源加载治理的第一难题就是异步IO怎么被Cancel掉,或者说从设计上怎么去实现等同的成果。 然而绝对于从Flash到Cocos到Unity来说,Unity还是做的很好的。之前的任何引擎要解决好这个问题,十分之艰难,须要很多代码的反对,包含多线程更低层次的相干代码。这一块Unity做得十分好,咱们不必搜索枯肠去实现异步IO Cancel的问题,只须要做好下层架构,设计出相似的性能。 Unity的异步加载会返回一个Request对象,该对象有一个Get属性AssetBundle,认真查看该Get函数API会发现。当调用该属性时,会促使底层异步IO中断(具体怎么实现的,有C++源码的能力晓得),并立刻同步加载该资源,且回调,回调程序是和你调用程序相当的(比方异步加载调用了2次并设置了回调,当初触发中断并立刻实现,会优先回调异步设置的回调)。 所以基于这个设计一个好的援用计数器治理资源十分重要。 大部分我的项目的援用计数器设计都是待资源实现后增加援用计数。这种设计的实用场景十分局限。在大型项目中应用环境非常复杂的资源加载状况下(异步加载中调用同步,异步加载中须要勾销,异步加载中勾销后立刻同步等)会呈现十分多的未知问题和开发中的局限性,这种设计只适宜微型我的项目疾速开发。 所以一个齐备的援用计数设计应该是调用加载办法时,就生成该资源将来被加载实现后的数据壳子,并增加援用计数(因为加载自身其实也是一次援用,包含bundle.AsynLoadAsset,其实任何的异步加载本身都须要增加一次援用,避免下层代码的逻辑,造成援用计数器非法=0触发bundle.unload办法),实现后缩小援用计数。那么同步加载时须要查找数据壳子,如果该资源正在异步加载,须要调用其request.assetBundle让其立刻中断异步并立刻同步实现加载。(否则同步加载时底层会抛出正告。) 基于这个咱们能够设计一套十分齐备的Cancel零碎,下层调用Cancel时,其实就是让其内部援用-1,因为实践上咱们能拿到的所有接口无奈真正中断一次IO,只有待其加载实现后,发现援用计数等于0时,才会Unload掉资源。 设计思路这里只是抛出一点,具体怎么去实现和设计畅所欲言。只是如果一个资源管理器不能适应任何(绝大部分)加载情景,那么这还是一个资源管理器吗? 感激1 9 7 3-311135@UWA问答社区提供了答复 LuaQ:为什么LuaDLL.lua_pcall()本身会产生这么大的开销? A1:是不是Lua本身逻辑耗费比拟高,这个能够看Lua侧的耗费:《Lua Profiler——疾速定位Lua性能问题》。感激jjjzzz@UWA问答社区提供了答复 A2:lua_pcall只是通过压栈的形式调用了Lua,具体耗费还得在Lua那边看看,预计是某函数C#和Lua疯狂调用比拟厉害。 感激萧小俊@UWA问答社区提供了答复 A3:用DeepProfile能力晓得具体是哪个办法调用高,这里只是C# call到Lua,前面的栈信息曾经没有了,不能单纯的说LuaDLL.lua_pcall()开销大。感激1 9 7 3-311135@UWA问答社区提供了答复 RenderingQ:法线在手机渲染时呈现的谬误问题:用的版本是Unity 2019.4 URP。Editor模式下是正确的,在默认Unity Build的打包正确,将资源打成AssetBundle后渲染呈现谬误。Shader应用了多种,包含默认的Lit Shader,都会呈现该问题。 A:狐疑是渲染管线的Shader问题,变体收集问题,最初一一排除后变成Mesh问题。有好大哥MoMo的奶爸通知我,Unity局部版本的Optimize Mesh Data选项默认关上,会优化掉局部网格,导致以上的问题。感激题主霸气九号@UWA问答社区提供了答复 RenderingQ:URP中Shader的UNITY_MATRIX_I_V 和C#中Camera.main.worldToCameraMatrix.inverse获取到的矩阵并不一样,在Shader中用ViewSpace的坐标乘以UNITY_MATRIX_I_V并不可能失去正确的世界坐标,乘以Camera.main.worldToCameraMatrix.inverse则能够,请问他们二者的区别是什么呢? A:我这边做的测试并没有发现这两个矩阵有什么不同。 fixed4 frag (v2f i) : SV_Target{ fixed4 col = UNITY_MATRIX_I_V[0]; return col;}在Shader中批改Frag来调用UNITY_MATRIX_I_V: void Start(){ Debug.Log(Camera.main.cameraToWorldMatrix); Debug.Log(m_camera.cameraToWorldMatrix); Debug.Log(Camera.main.worldToCameraMatrix.inverse);}在C#中打印这个矩阵,从Frame Debugger和控制台输入的后果来看这两个矩阵没有什么不同。 应用URP的Shader Simple Lit,计算逆矩阵之后的后果也是统一的: 感激宗卉轩@UWA问答社区提供了答复 ...

November 1, 2021 · 1 min · jiezi

关于lua:mac上安装lua

一、背景最近在操作redis的时候,有些时候是须要原子操作的,而redis中反对lua脚本,因而为了当前学习lua,此处记录一下 lua的装置。 二、mac上装置lua其余的零碎上装置lua步骤大略类似。 1、拜访lua下载页面拜访 lua下载链接 https://www.lua.org/download.html 2、通过源码装置lua1、下载luacurl -R -O http://www.lua.org/ftp/lua-5.4.3.tar.gz2、解压tar -zxvf lua-5.4.3.tar.gz3、进入lua目录cd lua-5.4.34、执行make命令make执行make命令,如果make出错,参考make出错的解决执行完make命令后在咱们的src目录下会存在lua、luac和liblua.a文件 5、检测lua是否构建正确$ make test ./lua -vLua 5.4.3 Copyright (C) 1994-2021 Lua.org, PUC-Rio输入了版本号阐明是构建正确的 6、装置lua $ sudo make all install Password:Guessing Darwin/Library/Developer/CommandLineTools/usr/bin/make all SYSCFLAGS="-DLUA_USE_MACOSX -DLUA_USE_READLINE" SYSLIBS="-lreadline"make[3]: Nothing to be done for `all'.cd src && mkdir -p /usr/local/bin /usr/local/include /usr/local/lib /usr/local/man/man1 /usr/local/share/lua/5.4 /usr/local/lib/lua/5.4cd src && install -p -m 0755 lua luac /usr/local/bincd src && install -p -m 0644 lua.h luaconf.h lualib.h lauxlib.h lua.hpp /usr/local/includecd src && install -p -m 0644 liblua.a /usr/local/libcd doc && install -p -m 0644 lua.1 luac.1 /usr/local/man/man1如果没有权限就加上sudo ...

October 20, 2021 · 1 min · jiezi

关于lua:不会-LuaPython-助你快速上手-Apache-APISIX-插件开发

相熟 Apache APISIX 的小伙伴都晓得,之前在社区中咱们曾经反对了 Java 和 Go 语言的 Runner,明天 Apache APISIX Python Runner 也来了,社区中的小伙伴们在开发 Apache APISIX 插件时又多了一种新抉择。 Python 语言作为一个解释型的高级编程语言,它语法简洁易上手、代码可读性好 ,在跨平台 、可移植性 、开发效率上都有很好的体现,同时作为一个高级编程语言它的封装形象水平比拟高屏蔽了很多底层细节(例如:GC )让咱们在开发的过程中能够更专一应用逻辑的开发。 同时作为一个有 30 年历史的老牌开发语言,它的生态以及各种模块曾经十分欠缺,咱们大部分的开发和利用场景都能够从社区中找到很成熟的模块或解决方案。 Python 其余的长处就不再一一赘述,当然它的毛病也比拟显著:Python 作为一门解释性语言,相较于 C++ 和 Go 这样的编译型语言,在性能上的差距还是比拟大的。 理解:我的项目架构apache-apisix-python-runner 这个我的项目能够了解为 Apache APISIX 和 Python 之间的一道桥梁,通过 Python Runner 能够把 Python 间接利用到 Apache APISIX 的插件开发中,最重要的还是心愿让更多对 Apache APISIX 和 API 网关感兴趣的 Python 开发者通过这个我的项目,更多地理解和应用 Apache APISIX,以下为 Apache APISIX 多语言反对的架构图。 上图右边是 Apache APISIX 的工作流程,左边的 Plugin Runner 是各语言的插件运行器,本文介绍的 apisix-python-plugin-runner 就是反对 Python 语言的 Plugin Runner。 ...

September 16, 2021 · 2 min · jiezi

关于lua:关于AI逻辑写在Lua中的问题

1)对于AI逻辑写在Lua中的问题2)Shader中宏的作用3)Update中的new Struct对象4)通过在编辑的预制体中获取资源门路 这是第266篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) LuaQ:当初战斗的外围逻辑都在xLua外面,蕴含了AI的逻辑,这部分计算量极大,会造成画面卡死。 尝试过用Thread把xLua虚拟机包一层,后果还是会报Main Thread的谬误。也尝试用闭包的形式,从Lua回传到CSharp,用System.Action接住,而后丢线程外面跑,然而会卡断主线程。 当初想尝试用两个Lua虚拟机,一个放Thread外面只跑战斗,其余的放在主Lua虚拟机中。那么整个游戏运行起来,会有3个虚拟机:Mono、xLua和xLuaInThread。 策动心愿AI做得聪慧一些,对应的思考维度和运算量都会大幅提高。不晓得是否有其余举荐的计划? A1:我感觉不是虚拟机方向的问题: Lua和Unity交互的细节没有把控好,比方重复在lua.transform.position等等相似的。不论是Tolua还是xLua,并不是说齐全就不必C#了。应该是基于几个准则:扩展性、热更性和性能,比方MMORPG的头顶UI必定不是全副是Lua开发的,业务在Lua然而挪动那些必定是C#(一旦做完了,接下来简直不会再动到C#代码了)。Lua和C#交叉的细节须要把控。Lua自身的应用细节也须要把控好。感激沈杰@UWA问答社区提供了答复 A2:不须要多个虚拟机。能够浏览《游戏编程精粹》里的这两篇文章(年代有点久了不记得是哪一本,也不记得具体讲了那些内容,只是取得了思路后,本人实现过): 用于游戏对象AI的微线程应用微线程治理AI我在已经的我的项目中实现基于协程的工作管理器,大抵有这些特点: 每个游戏对象运行的脚本在独立创立的协程上。每个协程入口对立采纳相似C语言的入口办法标准,如: 入口函数Main。随眠函数Sleep,Yield,Waitframe,Waittime(这些办法能够准确管制AI的计算频率与精度)。退出相干办法Exit,Atexit。对立在宿主程序中,对游戏中多个协程进行按需调度,比方期待指定工夫,帧数后调用Resume唤醒协程。感激lujian@UWA问答社区提供了答复 ShaderQ:Particle Additive Shader中应用了该宏:UNITY\_DECLARE\_DEPTH\_TEXTURE(\_CameraDepthTexture);,查了一下没找到相干阐明。字面了解是申明了一个深度图。 在默认管线(向前渲染)中,依照惯例了解深度图须要通过设置Camera的depthTextureMode来指定。还请各位指教。 A:Shader中很多宏确实是没有官网阐明的,咱们须要去CGInclude文件夹中寻找其确切定义。 UNITY_DECLARE_DEPTH_TEXTURE在HLSLSupport.cginc里: 确实是为了申明一张图,回到Particle Add.shader中发现是为了申明深度图: 这是为了在片元着色器中的深度采样做筹备: 该SAMPLE_DEPTH_TEXTURE_PROJ办法同样在HLSLSupport.cginc中: 而后持续在该文件中找tex2Dproj: 由此可见这些操作就是在相机保留的深度图中,通过tex2D获取了深度,为LinearEyeDepth筹备参数从而失去场景深度。该Shader通常在开了软粒子之后被用到。 感激翟孟飞@UWA问答社区提供了答复 ScriptQ:Struct中蕴含String字段,如果在Update中new,会不会有GC问题?思考是否须要用缓存池来解决,目前我测试了一下将Struct改成Class是必定有,如果是Struct类型,就没找到。 A1:对Class进行new是会调配堆内存,然而这个堆内存不肯定是String字段造成的。Class的实例自身就会占用堆内存,而它的字段会不会造成堆内存调配,取决于new的时候会不会对类型进行实例化。实例化的时候给String赋值,如果是间接确定的字符串内容,编译器会主动进行缓存优化,只会在第一帧创立字符串分配内存。 至于Struct,构造体作为值类型自身不会占用堆内存。对Struct进行new会不会调配堆内存,取决于new的时候,它的字段会不会实例化,会不会占用堆内存。如果Struct没有写构造函数,即便外面有援用类型,在new Struct的时候也不会实例化,不会调配堆内存。如果写了构造函数,就要看构造函数外面是否调配堆内存了。String的状况与上文提到的统一。 感激Prin@UWA问答社区提供了答复 A2:C#语法中除了构造函数造成的堆内存调配,还有构造体对外传递造成栈上逃逸问题。具体表现是结构的构造体作用域不仅局限在Update函数内。 在Unity的Playable模块里能够见到大量的ref Struct签名,是C# 7里专门解决栈逃逸的语法,依葫芦画瓢即可。 感激陈xx@UWA问答社区提供了答复 ScriptQ:我想调用AssetDatabase.GetAssetPath 获取资源门路,但我点击图1物体,应用AssetDatabase.GetAssetPath(gameObject),却获取不到。想请问:如何通过图1获取到图2对应的物体门路? A1:PrefabUtility.GetNearestPrefabInstanceRoot用来获取最近的预制体实例;Root PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot用来获取预制体门路。 [MenuItem("Tools/GetPrefabInstancePath")] public static void GetPrefabInstancePath() { if (null != Selection.activeGameObject) { if (PrefabUtility.IsPartOfPrefabInstance(Selection.activeGameObject)) { var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(Selection.activeGameObject); Debug.Log("Prefab Path:" + path); } } }感激马三小伙儿@UWA问答社区提供了答复 ...

September 13, 2021 · 1 min · jiezi

关于lua:Air302-关于-pmHIB-的休眠定时唤醒

合宙 Air302 模组应用中国移动 NB 卡 cmnbiot2 模板时, T3324 (activeTime) 不为 0 引起的休眠唤醒问题 (对于模组想尽快进入低功耗休眠状态时的利用场景) 对于 PSM 的阐明(尤其 T3324) 问题复现 : 当应用以下代码会呈现意料之外休眠和唤醒,导致无奈实现精准的休眠唤醒PROJECT = "lowpower"VERSION = "1.0.0"_G.sys = require("sys")sys.taskInit(function() log.info("===========================", "pm", "lastReson", pm.lastReson()) while not nbiot.isReady() do sys.wait(100) end log.info("--------------------------------------- begin") pm.dtimerStart(0, 5 * 60 * 1000) pm.request(pm.HIB) if pm.check() then log.info("pm", "it is ok to hib") end log.info("--------------------------------------- end")end)sys.run()意外的唤醒: luat.pm Go into Sleep2luat.pm poweron: Wakup Sleep2 by RTC 255 初步解决 : 在代码中设置 TAU 为 0, nbiot.setPSM(1, 86400, 0) (模组设置基站核心网 T3324 工夫)PROJECT = "lowpower"VERSION = "1.0.0"_G.sys = require("sys")sys.taskInit(function() nbiot.setPSM(1, 86400, 0) -- 留神这里!! log.info("===========================", "pm", "lastReson", pm.lastReson()) while not nbiot.isReady() do sys.wait(100) end log.info("--------------------------------------- begin") pm.dtimerStart(0, 5 * 60 * 1000) pm.request(pm.HIB) if pm.check() then log.info("pm", "it is ok to hib") else log.info("pm", "force to hib") pm.force(pm.HIB) end log.info("--------------------------------------- end")end)sys.run()模组第一次上电或复位运行驻网,【可能会进入一次 `luat.pm Go into Sleep2` 、`luat.pm poweron: Wakup Sleep2 by RTC 255`】 之后再次进入`luat.pm Go into Hib`,尔后模组才将依照唤醒工夫准时唤醒(模组首次驻网设置 T3324 未失效)最终解决 : SIM 卡管理平台设置 T3324 为 0 [Net 状态指示灯燃烧不再闪速] ...

September 8, 2021 · 1 min · jiezi

关于lua:Lua与C传参

1)Lua与C#传参2)Unity公布iOS版本呈现屏幕问题3)安卓的View成为Unity界面的一部分4)Unity降级导致我的项目启动工夫过长5)Unity中Cascaded Shadows在挪动端的设置 这是第258篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) LuaQ:游戏中是先在Lua中String.format好再传入C#,还是在Lua里把参数传入C#,在C#里用String.format好? 即一个是Lua加载文字表,String.format后传给C#显示,另一个是C#读表,Lua把参数传给C#,再string.format应用,哪个形式更好些,或者有没有其余更好的形式? A1:搬运群里大佬的问答:第二种比拟好,因为传参越简略越好,参数能够打包传。感激Tao@UWA问答社区提供了答复 A2:能够参考一下这篇文章:《用好Lua+Unity,让性能飞起来——Lua与C#交互篇》,总之Lua与C#传参,优先思考int、float和double等类型,其次bool、string及object等类型,同时尽可能升高交互次数。感激羽飞@UWA问答社区提供了答复 iOSQ:Unity公布iOS版本在iOS 11之后呈现屏幕显示问题:半屏被压缩,另一半黑屏。 A:间接在编辑器中设置这几个参数,不要在代码中设置这个,7p就不会呈现这个问题:Screen.autorotateToLandscapeLeft = true; Screen.autorotateToLandscapeRight = true; Screen.autorotateToPortrait = false; Screen.autorotateToPortraitUpsideDown = false; Screen.orientation = ScreenOrientation.AutoRotation;感激许家胜@UWA问答社区提供了答复 UnityQ:加载了一个安卓的Activity,会遮挡住Unity的界面,如何让安卓的View,成为Unity界面的一部分呢?Unity界面的一部分应用安卓原生SDK的界面实现。 A:安卓混合Unity的界面,曾经实现了。https://blog.csdn.net/Yzw_92_...感激题主韩飞@UWA问答社区提供了答复 EditorQ:查看了Unity的editor.log发现外面: RefreshInfo: InitialScriptRefreshV2(NoUpdateAssetOptions)RefreshProfiler: Total: 0.000msInvokeBeforeRefreshCallbacks: 1115393.587ms...... OnSourceAssetsModified: 340.325ms...... ImportAndPostprocessOutOfDateAssets: 1032441.185ms (82533.117ms without children)ImportManagerImport: 82501.381ms (0.000ms without children)...... 有没有大佬晓得这个是在解决什么货色?迷信上网了也没有查到相干信息,这里看耗时就很长时间了1115393.587ms。 A:参考以下几点: 检查一下我的项目是不是放在机械硬盘上,倡议放NVME的固态上。因为高版本的数据结构更简单了,不必固态来不及序列化。检查一下是不是开了PlasticSCM。感激萧小俊@UWA问答社区提供了答复 RenderingQ:Unity的Shadow Cascade性能在挪动平台中是否失效? 在场景应用了Unity自带的实时暗影,具体参数如下: 用Frame Debug察看暗影绘制过程: Shadowmap为1024*1024,场景远处会有彩色的暗影笼罩。当Cascade设置为2则远处暗影隐没: 用Frame Debug察看暗影绘制过程: Shadowmap还是1024*1024,但一分为二,各占1024*512。 以上是在PC上验证的,证实Cascade的确起作用。但当在Unity编辑器中的BuildSettings中切换到Android平台时,无论怎么批改Cascade的值,场景远处均有暗影,用Frame Debug察看暗影绘制过程如下: 仿佛在安卓平台中这个参数不起作用,查阅Unity官服文档也并没阐明Shadow Cascade不反对挪动平台。 请问有同样遇到这种问题的吗? A:Graphics设置那里须要在对应的Tier外面启用Cascaded Shadows。感激题主x21@UWA问答社区提供了答复 20210712更多精彩问题等你答复~ 1.如何实现AAB包的增量更新2.对于Unity出AAB包的问题3.RTS 手游技术难点 封面图来源于网络 ...

July 19, 2021 · 1 min · jiezi

关于lua:LuaJIT-是如何工作的-JIT-模式

上一篇 咱们说到,JIT 是 LuaJIT 的性能杀手锏。这篇咱们就介绍一下 JIT。 Just-in-time 即时编译技术,在 LuaJIT 里的具体应用是:将 Lua byte code 即时编译为机器指令,也就是不再须要解释执行 Lua bytecode,间接执行即时编译产生的机器指令。也就是说,解释模式,和 JIT 模式的输出源是一样的,都是 Lua byte code。雷同的字节码输出,两种模式却能够有跑出显著的性能区别(一个数量级的区别,也是比拟常见的),这个还是很须要功力的。 JIT 能够分为这么几个步骤: 计数,统计有哪些热代码录制,录制热代码门路,生成 SSA IR code生成,SSA IR code 优化生成机器指令执行新生成的机器指令JIT 编译对象在进一步开展之前,先介绍一个基本概念。LuaJIT 的 JIT 是基于 trace 的,意味着一段 byte code 执行流,而且是能够逾越函数的。相比较而言,Java 的 JIT 是基于 method 的,尽管也有函数内联,然而限度比拟大(只有很小的函数才会被内联 JIT 编译)。 集体认为,tracing JIT 在实践上来说,能够比 method base JIT 有更大的施展空间,如果只是某些 case 跑分,应该能够更厉害。不过工程实现复杂程度要高不少,所以最终的理论工业成果,也难说(影响 JIT 成果的,还有很多其余因素,比方优化器等等)。 比方这个小示例: local debug = falselocal function bar() return 1endlocal function foo() if debug then print("some debug log works") end return bar() + 1end当 foo() 函数被 JIT 编译的时候,有两个显著的长处: ...

June 23, 2021 · 2 min · jiezi

关于lua:Lua-代码是如何跑起来的

上一篇「C 代码是如何跑起来的」中,咱们理解了 C 语言这种高级语言是怎么运行起来的。 C 语言尽管也是高级语言,然而毕竟是很 “古老” 的语言了(快 50 岁了)。相比较而言,C 语言的抽象层次并不算高,从 C 语言的表达能力里,还是能够领会到硬件的影子。 旁白:通常而言,抽象层次越高,意味着程序员的在编写代码的时候,心智累赘就越小。明天咱们来看下 Lua 这门绝对小众的语言,是如何跑起来的。 解释型不同于 C 代码,编译器将其间接编译为物理 CPU 能够执行的机器指令,CPU 执行这些机器执行就行。 Lua 代码则须要分为两个阶段: 先编译为字节码Lua 虚拟机解释执行这些字节码旁白:尽管咱们也能够间接把 Lua 源码作为输出,间接失去执行输入后果,然而实际上外部还是会别离执行这两个阶段字节码在「CPU 提供了什么」 中,咱们介绍了物理 CPU 的两大根底能力:提供一系列寄存器,能执行约定的指令集。 那么相似的,Lua 虚拟机,也同样提供这两大根底能力: 虚构寄存器执行字节码旁白:Lua 寄存器式虚拟机,会提供虚构的寄存器,市面上更多的虚拟机是栈式的,没有提供虚构寄存器,然而会对应的操作数栈。咱们来用如下一段 Lua 代码(是的,逻辑跟上一篇中的 C 代码一样),看看对应的字节码。用 Lua 5.1.5 中的 luac 编译能够失去如下后果: $ ./luac -l simple.luamain <simple.lua:0,0> (12 instructions, 48 bytes at 0x56150cb5a860)0+ params, 7 slots, 0 upvalues, 4 locals, 4 constants, 1 function 1 [4] CLOSURE 0 0 ; 0x56150cb5aac0 2 [6] LOADK 1 -1 ; 1 # 将常量区中 -1 地位的值(1) 加载到寄存器 1 中 3 [7] LOADK 2 -2 ; 2 # 将常量区中 -2 地位的值(2) 加载到寄存器 1 中 4 [8] MOVE 3 0 # 将寄存器 0 的值,挪到寄存器 3 5 [8] MOVE 4 1 6 [8] MOVE 5 2 7 [8] CALL 3 3 2 # 调用寄存器 3 的函数,寄存器 4,和寄存器 5 作为两个函数参数,返回值放入寄存器 3 中 8 [10] GETGLOBAL 4 -3 ; print 9 [10] LOADK 5 -4 ; "a + b = " 10 [10] MOVE 6 3 11 [10] CALL 4 3 1 12 [10] RETURN 0 1function <simple.lua:2,4> (3 instructions, 12 bytes at 0x56150cb5aac0)2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions 1 [3] ADD 2 0 1 # 将寄存器 0 和 寄存器 1 的数相加,后果放入寄存器 2 中 2 [3] RETURN 2 2 # 将寄存器 2 中的值,作为返回值 3 [4] RETURN 0 1略微解释一下: ...

May 3, 2021 · 2 min · jiezi

关于lua:Lua-OpenResty容器化考古历程

原文地址:Lua OpenResty容器化(考古历程) 背景公司有几个“远古期间”的我的项目,始终都绝对较为稳固,然而我的项目每天总会在一些时段,申请每分钟QPS达到峰值800K左右,导致机器的性能呈现了一些瓶颈,每到峰值期间,总会呈现一个告警,切实是令人头疼。更蹩脚的是这只是远古期间我的项目中的其中一个而且都是部署在物理机器上,所有机器加起来靠近100台。 出于稳定性(削峰)和老本的角度思考,咱们最终决定将所有的Lua OpenResty我的项目上到k8s集群。 抉择适合的openresty根底镜像通过查看线上在应用的openresty版本信息: /usr/local/openresty/nginx/sbin/nginx -Vnginx version: openresty/1.13.6.2built by gcc 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)built with OpenSSL 1.1.0h 27 Mar 2018 (running with OpenSSL 1.1.0k 28 May 2019)TLS SNI support enabledconfigure arguments: --prefix=/usr/local/openresty/nginx ...lua -vLua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio得悉在应用的是openresty/1.13.6.2和Lua 5.1.4 : docker pull openresty/openresty:1.13.6.2-2-centosQ:能不能抉择应用更小的alpine系列的呢? A:因为我的项目依赖许多的so库,都是glibc编译的,alpine的话是musl-lib,不兼容。 Q:为啥不从新编译? A:一方面是危险问题,另外一方面是有一些so库不肯定能找到。 查找我的项目的动静库依赖关系Nginx配置文件$ tree -L 3 nginx/confnginx/conf├── vhosts/│ ├── inner.prometheus.nginx.conf│ └── project.nginx.conf└── nginx.conf自编译的C动静库文件,如binary_protocol.so编写好dockerfile,而后将我的项目打包进容器,执行: /usr/local/openresty/nginx/sbin/nginx nginx -t果不其然,报错: /usr/local/openresty/nginx/lua/init.lua:1: module 'binary_protocol' not found:no field package.preload['binary_protocol']no file '/usr/local/openresty/nginx/lua/binary_protocol.lua'no file '/usr/local/openresty/nginx/lua_lib/binary_protocol.lua'no file '/usr/local/openresty/nginx/luarocks/share/lua/5.1/binary_protocol.lua'no file '/usr/local/openresty/site/lualib/binary_protocol.ljbc'…… ……no file '/usr/local/openresty/nginx/luarocks/lib64/lua/5.1/binary_protocol.so'no file '/usr/local/openresty/site/lualib/binary_protocol.so'no file '/usr/local/openresty/lualib/binary_protocol.so'no file '/usr/local/openresty/site/lualib/binary_protocol.so'no file '/usr/local/openresty/lualib/binary_protocol.so'no file './binary_protocol.so'no file '/usr/local/lib/lua/5.1/binary_protocol.so'no file '/usr/local/openresty/luajit/lib/lua/5.1/binary_protocol.so'no file '/usr/local/lib/lua/5.1/loadall.so'no file '/usr/local/openresty/luajit/lib/lua/5.1/binary_protocol.so'Q:仔细观察,发现so动静库是外部编译进去提供给lua调用的,如何找到它们呢? ...

April 21, 2021 · 2 min · jiezi

关于lua:lua远程调试-Remote-Debug

原文地址:lua近程调试 Remote Debug 日常的debug当把一个本地我的项目部署到近程测试服务器后有可能呈现意想不到谬误,为了排查问题可能会变成: 这样: 而后这样: 最初就: 最可怕的是,因为堆栈的关系,很难在一次debug日志中拿到想要的信息,往往是一层层往下打日志,能力拿到想要的debug信息。 remote debug本地服务器凋谢端口,将近程服务器的断点信息打到本地服务器。 那具体如何实现呢?jetbrains的“EmmyLua”插件 + mobdebug库 本地jetbrains减少EmmyLua插件装置 远端服务器减少mobdebug包放到我的项目debug目录下,并减少配置信息https://github.com/pkulchenko... local mobdebug = require("debug.mobdebug");mobdebug.rbasedir("/usr/local/openresty/nginx/lua/") -- remotemobdebug.lbasedir("/Users/wilburxu/lua/test/") -- localmobdebug.start("host.docker.internal", 28172); ps:断点信息发回的是远端服务器的line,所以本地服务器要保障和远端服务器的line统一。 本地增加调试configuration 发送申请就能够失去咱们想要的堆栈信息了。 MobDebug的根本构造mobdebug是一个纯lua实现的近程调试器,依赖于luasocket,根本的通信形式是应用字符串的形式在目标程序和IDE之间传输相应的控制指令和执行后果,mobdebug与远端交互的数据是间接包装成Lua格局的字符串的。 交互协定mobdebug应用的通信模式是应答式的,也就是大部分时候都是远端的IDE向调试目标程序发送一条命令后,就进入期待调试指标返回后果的状态了,在EmmyLua源代码侧的体现就是保护了一个Command队列,如果Command是须要应答的,那只有以后Command被解决完后,才会接着发送队列中残余的Command。

April 20, 2021 · 1 min · jiezi

关于memory:下载AssetBundle的Mono内存问题

1)下载AssetBundle的Mono内存问题2)Unity 2019运行时获取Hierarchy上预制体资源门路3)多个Submeshes模型合并后的显示问题4)ToLua中拜访Time.deltaTime为05)CacheServer莫名的断开连接 这是第242篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) MemoryQ:应用协程+UnityWebRequest下载Bundle时调配的内存开释不掉。 下载步骤如下:1. 应用协程+UnityWebRequest下载Bundle;2. 应用BinaryWriter将UnityWebRequest.downloadHandler.data的数据写入机器;3. 调用UnityWebRequest的Dispose函数;4. 调用Resources.UnloadUnusedAssets(),UnityWebRequest.ClearCookieCache()和System.GC.Collect()。 下载每一个Bundle都会执行如上流程,然而Profiler调试真机发现,调配的内存无奈开释,会把Mono内存越撑越大,请问大家遇到过这种状况吗? A:之前遇到过相似的问题,题主能够参考一下,咱们应用的Unity版本是2018.4.31。不论是之前的WWW还是当初的WebRequest,都是应用的其成员DownloadHandler进行下载。当你拜访“.data”属性时,其实拜访的是一个“GetData()”函数的包装,这个函数返回的是一段native-memory data buffer的拷贝,这也是问题的根结所在。 第一个能够优化的点就是缩小“.data”的应用,用长期变量缓存下来。第二个就是替换DownloadHandler,Unity提供了多种DownloadHandler,咱们过后是下载图片,所以用的是DownloadHandlerTexture,看你的问题,能够试试DownloadHandlerAssetBundle。 参考:https://docs.unity3d.com/ScriptReference/Networking.DownloadHandlerAssetBundle-ctor.html 感激Joke@UWA问答社区提供了答复 EditorQ:美术心愿在编辑器运行的状态下获取场景GameObject实例对应预制体的门路,以前Unity 2017是能够通过var pRoot = PrefabUtility.GetPrefabParent(go); return AssetDataBase.GetAssetPath(pRoot),获取运行时预制门路的。 最近降级到2019发现接口曾经更新,PrefabUtility.GetPrefabInstanceHandle(targetGameObject)、PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefab)等接口都不能在Unity运行的时候去获取Hierarchy外面的GameObject门路。 我问题外面的要害是运行状态,也就是点了播放,而后获取Hierarchy上预制的存储门路。PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefab)不点播放是能够获取,播放了只能获取Project上预制的存储门路。 A:PrefabUtility.GetNearestPrefabInstanceRoot:获取最近的预制体实例Root。PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot:获取预制体门路。 if (PrefabUtility.IsPartOfPrefabInstance(seletedGo)){ string prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(seletedGo);} GetPrefabAssetPathOfNearestInstanceRoot能够获取门路,“PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefab)” 这个接口传的不是Prefab局部,而是实例的。 如果不行,在Editor时,在它身上挂脚本或者别的形式建设映射,Runtime下再拿。 感激静风霁@UWA问答社区提供了答复 MeshQ:求教下,我测试一个Skinmesh合并性能,发现当Mesh蕴含多个Submeshes时,合并胜利后,无奈渲染显示进去。 当测试的Mesh不蕴含Submeshes时,能够合并显示。难道CombineMeshes只能合并不带Submeshes的Mesh?不晓得问题出在哪?有什么解决思路吗? 注:不论是应用“r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false); ”带上所有材质,还是“r.sharedMesh.CombineMeshes(combineInstances.ToArray(), true, false) ;”合并一张贴图材质,带多个Submeshes的模型只能合并无奈渲染显示进去。不带多个Submeshes的模型可能失常合并显示。 A:测试发现,须要对每个CombineInstance从新指定三角面,能力失常显示带多个Submeshes的模型。 如这段代码: foreach (SkinnedMeshRenderer smr in allSkineMeshList) { for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++) { CombineInstance ci = new CombineInstance(); ci.mesh = smr.sharedMesh; ci.mesh.triangles = smr.sharedMesh.triangles; //这里从新指定 ci.subMeshIndex = sub; ci.transform = matrix * smr.transform.localToWorldMatrix; combineInstances.Add(ci); } } 感激题主牛头人不服@UWA问答社区提供了答复 ...

March 17, 2021 · 1 min · jiezi

关于lua:luarockscn-中国首个-Lua-模块代理服务

LuaRocks 是应用最宽泛的 Lua 模块管理工具,与之绝对应的则是 https://luarocks.org,一个公共的 LuaRocks 服务。用户或通过 https://luarocks.org 下载其所需的 Lua 模块,或向 https://luarocks.org 上传本人开发的 Lua 模块。 然而该服务并没有设立中国站点,这意味着每当用户须要下载或者上传一个 Lua package 的时候,可能须要和位于远在太平洋彼端的服务器进行通信,延时之高可想而知,尤其对于像 Apache APISIX 这样蕴含泛滥 Lua 依赖模块的我的项目来说,拉取依赖所破费的工夫更是令人咋舌,这大大降低了应用 LuaRocks 的体验。 干流科技近日推出了 luarocks.cn 服务,该服务旨在减速国内用户拜访 https://luarocks.org 的速度。该服务面向所有开发者,且完全免费。只须要简略的配置即可应用该服务。例如,在下载某一个 Lua 模块的时候,在 --server 参数中指定 https://luarocks.cn 即可应用到该代理服务,如下两条命令通过 luarocks.cn 装置了 Apache APISIX 和 Kong。 $ luarocks install apisix --server https://luarocks.cn $ luarocks install kong --server https://luarocks.cn 此外,你能够通过批改 LuaRocks 的配置文件,如 ~/.luarocks/config.lua 和 ~/.luarocks/upload_config.lua (别离用于下载和上传),来无缝应用 luarocks.cn 服务。 # cat .luarocks/upload_config.lua rocks_servers = { "https://luarocks.cn"}# cat ~/.luarocks/upload_config.luakey = "<Your API Key>"server = "https://luarocks.cn"

March 8, 2021 · 1 min · jiezi

关于lua:从Lua中的字符串中提取数字

能够应用%D+删除string.gsub(1个或多个非数字字符)模式前面的所有匹配项: s = "($1,000)"res, _ = s:gsub("%D+", "")print(res)-- => 1000 请参阅Lua demo

March 5, 2021 · 1 min · jiezi

关于lua:Lua全局变量代码规范

1)Lua全局变量代码标准2)AssetBundle LockPersistentManager开销3)Unity内置字体在资源检测报告中不算冗余资源4)特定Android设施上,Adreno产生冻屏问题5)Mask和RectMask性能上的区别 这是第238篇UWA技术常识分享的推送。明天咱们持续为大家精选了若干和开发、优化相干的问题,倡议浏览工夫10分钟,认真读完必有播种。 UWA 问答社区:answer.uwa4d.comUWA QQ群2:793972859(原群已满员) LuaQ:应用Lua语言作为脚本辅助开发曾经十分风行了,然而Lua语言中的全局变量是一个令人头疼的问题,因为无需申明就能够应用、编译器编译不会针对重命名和笼罩进行报错,稍不留神就会笼罩掉全局变量导致Bug,而且全局变量援用GameObject有可能会造成泄露。 大家在开发过程中,对于Lua全局变量会制订什么代码标准吗?例如:什么时候能够应用全局变量?如何申明?如何躲避笼罩等问题,谢谢。 A:能够在Lua虚拟机启动当前,在适当的机会执行一下luaGlobalCheck.lua文件,这个文件外面会设置一下_G的元表和元办法,通过重写_newindex和 _index元办法的形式来做到禁止新建全局变量和拜访不存在的全局变量时提醒谬误。这样能够做到防止随便新建全局变量污染环境和笼罩的问题。 luaGlobalCheck.lua代码如下: setmetatable(_G, { -- 管制新建全局变量 __newindex = function(_, k) error("attempt to add a new value to global,key: " .. k, 2) end, -- 管制拜访全局变量 __index = function(_, k) error("attempt to index a global value,key: "..k,2) end}) 感激马三小伙儿@UWA问答社区提供了答复 AssetBundleQ:察看性能曲线,发现某一帧AssetBundle加载中,LockPersistentManager耗时比拟大。请问这块是否可能优化? A1:这阐明以后帧或前几帧中存在较大量的资源在通过LoadAsync来进行加载,其本质是所加载的资源过大所致,对本身资源进行正当优化可升高Loading.LockPersistentManager的开销。另外,将异步加载换成同步加载,LockPersistentManager就不会呈现了,但其总加载耗时是没有变动的,因为总加载量没变。对于次要资源的加载优化,可参考如下链接:《Unity加载模块深度解析(纹理篇)》《Unity加载模块深度解析(网格篇)》《Unity加载模块深度解析(Shader篇)》《Unity加载模块深度解析之动画片段》《移动游戏加载性能和内存治理全解析》 该答复由UWA提供 A2:Unity 2019.4.1版本下,其实是bundle.LoadFromFileAsync在主线程的Integrate Asset中执行,和PreloadManager线程的LoadAssetAsync不能同时进行,必须要锁,也就呈现LockPersistentmanager,始终锁到一方完结。实质还是这块实现不欠缺,能够用Spin Lock始终锁到Application.backgroundLoadingPriority规定的工夫再到下一帧就行了,不必始终锁到一方开释。 Unity 2019.4.11和2019.4.16批改了主线程读Bundle和等锁的问题: 我在Unity 2020.1.17版本出的iOS上测试是根本解决了: 然而还有个别异步加载主线程等锁的景象,预计是资源太大收集依赖工夫太长触发的: 感激燃野@UWA问答社区提供了答复 AssetBundleQ:咱们的我的项目工程中返现两个界面的Prefab,都是用Unity本人的Arial字体生成的Bundle,上传到资源检测,然而在报告中并未看到内置的字体是冗余资源。 A1:这个Arial是属于Unity内置资源,打包APK的时候是会被打进unity default resources外面的,所以AssetBundle中应用到了这个Arial字体都是援用关系,并不会打包进对应的AssetBundle中,因而看不到冗余是失常的。应用AssetStudio关上APK包中的assets/bin/Data/目录下的unity default resources就能够看到了,如下图: ...

February 26, 2021 · 1 min · jiezi

关于lua:基于openresty的web-api框架

用openresty搭建的一个繁难web api框架,不便之后用的时候疾速生成我的项目构造 我的项目地址点击这里 目录构造构造供蕴含config、controller、libs、model四个目录 config 配置文件目录,用于app、redis、database相干的配置 app利用相干return { default_controller = 'home', -- 默认控制器 default_action = 'index', -- 默认办法}数据库相干local mysql_config = { timeout = 5000, connect_config = { host = "127.0.0.1", port = 3306, database = "demo", user = "root", password = "a12345", max_packet_size = 1024 * 1024 }, pool_config = { max_idle_timeout = 20000, -- 20s pool_size = 50 -- connection pool size }}redis配置return { host = "127.0.0.1", -- redis host port = 6379, -- the port max_idle_timeout = 60000, -- max idle time pool_size = 1000, -- pool size timeout = 1000, -- timeout time db_index= 2, -- database index }libs目录libs目录上面的公共的模块库,包含redis、db、request、response等 ...

January 1, 2021 · 2 min · jiezi

关于lua:lua和C语言互相调用的例子

之前都是看的lua脚本怎么写的,还有就是在之前的工作室仿照着写。没有真正懂LUA和C语言如何之间交互的。明天写了一个例子 首先我要实现的性能计算两个整数的和,就是x+y了。因为x和y是随时变动的,我又不想在C程序里批改,那么就把这两个参数放到lua脚本来传递。 那么我是如何实现的呢? 第一步:搭建window环境,应用vs2005 这个货色在网上搜寻一下吧,很多。 第二步:写一下c函数的逻辑。 //testlua.c#include "stdafx.h"#include <stdio.h>extern "C" {#include "lua.h"#include "lualib.h"#include "lauxlib.h"}lua_State* L;int add(lua_State* L);int add(lua_State* L){//从L栈中取出索引为1的数值,并查看int x = luaL_checkint(L,1);//从L栈中取出索引为2的数值,并查看int y = luaL_checkint(L,2);printf("result:%dn",x+y);return 1;}int _tmain(int argc, _TCHAR* argv[]){//初始化全局LL = luaL_newstate();//关上库luaL_openlibs(L);//把函数压入栈中lua_pushcfunction(L, add);//设置全局ADDlua_setglobal(L, "ADD");//加载咱们的lua脚本文件if (luaL_loadfile(L,"E:workvsProjecttestLuamylua.lua")){printf("errorn");}//安全检查lua_pcall(L,0,0,0);//push进lua函数lua_getglobal(L, "mylua");lua_pcall(L,0,0,0);printf("hello my luan");return 0;} x上面是我的lua脚本代码,很简略 function mylua()print("mylua")ADD(1,2)ADD(3,4)end ADD(1,2) 关联到注册到的add函数,把参数压入了。 最初输入后果为: 看是不是很简略啊

November 11, 2020 · 1 min · jiezi

关于lua:我们一起来学lua协程coroutine-四

明天咱们先来看下lua手册上一个协程实例: 手册实例:function foo(a) print("foo", a) return coroutine.yield(2 * a)endco = coroutine.create(function ( a, b ) print("co-body", a, b) local r = foo(a + 1) print("co-body", r) local r, s = coroutine.yield(a + b, a - b) print("co-body", r, s) return b, "end"end)print("main", coroutine.resume(co, 1, 10))print("main", coroutine.resume(co, "r"))print("main", coroutine.resume(co, "x", "y"))print("main", coroutine.resume(co, "x", "y"))执行后果:co-body 1 10 -- 协程co的第7行,此时resume()传入的参数是赋值给了函数的foo 2 -- 在第8行外面调用了函数foo(),执行到第2行的打印main true 4 -- 因为函数foo()的第3行yield()执行后挂起,参数是4,作为第15行的resume()的第二个返回值,最终打印了进去,到此,第15行执行结束co-body r -- 第16行resume()再次唤醒协程co,接着上次yield()的中央继续执行,参数“r"被赋值给上次yield()的返回值,在第9行打印进去main true 11 -9 -- 在第10行yiled()后再次挂起协程co,并返回,此时参数a和b还是第一次resume()时的参数,1,10,所以yield()两个参数别离为11,-9,作为resum()的第二个返回值,最终被打印进去,到此,第16行执行结束co-body x y -- 第17行resume()再次唤醒协程co,传入的参数“x”,“y”被赋值给上次的yield()函数的返回值,即赋值给第10行的r,s,在第11行被打印进去main true 10 end -- 协程co在第12行返回,留神此时参数b依然是第一次resume()时的参数2,值为10,至此协程co执行完结,变为dead状态,最终在第17行打印进去main false cannot resume dead coroutine -- 第18行尝试再次resume()协程co,因为协程co曾经为dead状态,所以间接返回并报错下面这个实例很好的展现了coroutine.yield和coroutine.resume之间的相互作用。协程在生产者和消费者问题上应该用也比拟宽泛,咱们来看看上面这个例子。 ...

September 23, 2020 · 1 min · jiezi

关于lua:我们一起来学lua协程coroutine-三

*上一期咱们次要介绍了协程的相干函数,以及协程的创立。这一期,咱们次要来介绍协程怎么进行合作的,而合作也正是协程的精髓所在,小伙伴们,小板凳带好了嘛?≧◠◡◠≦✌*生存当中咱们做事件常常都是不可能一干到底的。比方,你正在看书,这时候忽然来了个电话,或者说你在跟朋友家人视频聊天,这时候忽然没信号了了。在这种状况下,咱们都不得不停下原先手中的事件,去解决其余事件,等其余事件解决完了,再接着原先的事件往下走。为了解决这样的问题,协程也提供了合作机制。咱们先来看看上面的一个简略实例。合作实例一:--创立一个协程,但还没有调用read_co = coroutine.create( function () print("看书") print("有电话打进来") coroutine.yield() print("电话打完了,持续看书") end)--开启读书协程coroutine.resume(read_co)print("打电话")coroutine.resume(read_co)运行后果:看书有电话打进来打电话电话打完了,持续看书实例通过coroutine.yield()挂起协程,而后又通过coroutine.resume切换回协程,持续往下执行。是不是很简略?接下来咱们来看下coroutine.yield跟coroutine.resume更深一步的做法,咱们还是间接看实例吧。 协程实例二:--创立一个协程,但还没有调用read_co = coroutine.create( function () print("看书") print("有电话打进来") local ret1, ret2 = coroutine.yield("我读到第三章了", "第二章很精彩") print("ret1 = "..ret1) print("ret2 = "..ret2) print("电话打完了,持续看书") end)--开启读书协程local ret1, ret2, ret3 = coroutine.resume(read_co)print(ret1)print("ret2 = "..ret2)print("ret3 = "..ret3)print("打电话")coroutine.resume(read_co,"王老师打来的电话","让我读第五章")运行后果: 看书有电话打进来trueret2 = 我读到第三章了ret3 = 第二章很精彩打电话ret1 = 王老师打来的电话ret2 = 让我读第五章电话打完了,持续看书从这个实例咱们能够看到coroutine.yield的参数能够返回给前一个coroutine.resume。这里须要留神的是coroutine.resume的第一个返回值是协程运行是否出错,通过打印咱们能够晓得这里要么等于true要么等于false。从第二个返回值开始才是coroutine.yield传进去的值。协程应用coroutine.yield挂起之后,如何使它继续执行呢?还是要通过coroutine.resume,这时候coroutine.yield的返回值也就是coroutine.resume的入参信息。这里童鞋们看明确了?好了明天协程内容就介绍到这里,今天咱们持续深入探讨协程的合作问题,以及合作在异步通信的具体利用~~~

September 22, 2020 · 1 min · jiezi

关于lua:我们一起来学lua协程coroutine-二

大家好,骚气十足的额,又来了,对,明天咱们次要来学习下lua协程的用法,小伙伴们筹备好小板凳,筹备开车了~~~(ps:如同有点偏题了,不过不重要你们当作没看见就行(O ^ ~ ^ O)) 协程次要函数:函数名入参返回值形容coroutine.create()承受单个参数,这个参数是coroutine的主函数返回它的控制器,(一个对象为thread)的对象创立 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合应用的时候就唤醒函数调用coroutine.resume()第一个参数:coroutine.create的返回值,即一个thread对象。第二个参数:coroutine中执行须要的参数,是一个变长参数,可传任意多个参数。如果程序没有任何运行谬误的话,那么会返回true,之后的返回值是前一个调用coroutine.yleld中传入的参数。如果有任何谬误的话,就会返回false,加上错误信息重启 coroutine(重启时不必再传参数),和 create 配合应用coroutine.yield()传入变长参数,这些参数会作为返回值被 coroutine.resume接管返回在前一个调用coroutine.resume()中传入的参数值挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合应用能有很多有用的成果(返回参数)coroutine.running()空返回以后正在运行的协程。如果它被主线程调用的话,会返回nil返回正在跑的 coroutine,一个 coroutine 就是一个线程,当应用running的时候,就是返回一个 corouting 的线程号如果没有返回nilcoroutine.status()空返回以后协程的状态:有running,suspended,normal,dead返回正在跑的 coroutine,一个 coroutine 就是一个线程,当应用running的时候,就是返回一个 corouting 的线程号如果没有返回nil创立协程实例: --创立一个协程co = coroutine.create( function (str) print(str); end)print("以后协程状态:"..coroutine.status(co))运行后果: 以后协程状态:suspended创立协程的函数是coroutine.create,调用这个函数胜利之后就会返回一个协程对象,后续咱们无关协程的操作就围绕这个对象进行。下面的例子co就是咱们协程对象。通过coroutine.status这个函数查看协程状态,咱们能够发现以后的状态是suspended挂起状态,也就是说coroutine.create只实现创立协程的动作,然而协程当初并没有跑起来,不然这时候的状态就应该是running了。为了让协程跑起来,这时候就轮到coroutine.resume这个函数大展拳脚了。咱们再来看下上面这hello world例子。(ps:人人都爱hello world 不要厌弃博主举的例子很low啊≧◠◡◠≦✌) hello world例子: --创立一个协程,但还没有调用co = coroutine.create( function (str) print(str); end)--开启协程,打印hello world!coroutine.resume(co,"hello world!")通过hello world这个例子咱们就能够分明的看到协程失常运行起来了,这时候协程的状态是runnning嘛?有趣味的童鞋能够打印进去看看,想想看为什么是这个状态。有时候我感觉,学习编程,最重要的还是大家可能多入手,多思考。不然就算我博客写得再具体,大家可能把握的货色还是很无限的。just do it~~~话不多说,干就是了! lua中文手册参考文章一参考文章二

September 21, 2020 · 1 min · jiezi

挖矿GO系统APP开发制作

RESTful API +挖矿GO系统APP开发制作的概述,T:I8O.2853.296O V黎的基本概念REST 英文全称:Representational State Transfer,直译为:表现层状态转移。首次是由Roy Thomas Fielding在他2000年的博士论文中提出。 REST是一种描述网络中client和server之间的资源交互方式。 而RESTful API就是完全遵循REST方式的一套API设计规范,简单来说,通过API来描述资源的访问方式: 通过HTTP URL描述访问什么资源通过HTTP METHOD描述对资源的交互方式通过HTTP CODE描述资源的交互结果幂等性幂等性(Idempotence)本身是一个数学概念,在HTTP/1.1规范中幂等性是指 Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request. 如果某个方法调用一次或多次产生的副作用是相同的,那么这个方法具有幂等性。比如在HTTP中使用GET获取某个资源,无论调用多少次,产生的额外效果都是从服务器获取资源,所以GET方式具有幂等性。 而POST方法用于在服务器上创建一个资源,由于最终创建的结果每次都是不同的,所以POST不具有幂等性。 但是PUT方法却是幂等的,因为每次调用产生的效果都是对资源进行更新。 安全方法安全方法是指不修改资源的 HTTP 方法。譬如,当使用 GET 或者 HEAD 作为资源 URL,都必须不去改变资源的表现形式。 注意:安全方法并不是指服务器上的资源完全不变,而是指资源的表现形式。 比如GET方法导致数据上报,关联的一些数据记录等,他实际上是改变了服务器上的某些附加资源的,但是这并不会改变资源的表现形式。 HTTP中的安全方法有: GETHEADPATCHRESTful API设计规则HTTP URLHTTP URL只用于描述访问的资源,而不应该包含对资源的交互方式。HTTP URL的Best Practice: ...

November 4, 2019 · 2 min · jiezi

野子电商数据分析

野子电竞数据官网改版https://www.xxe.io/ 全新登场1.通用型的数据分析入门思维,比如AARRR(海盗模型)获取用户–>提高活跃度–>提高留存率–>获取营收–>自传播 2.实现数据分析的流程深入业务–>构建指标体现–>事件设计–>数据采集–>业务目标–>数据分析–>验证迭代重点:1)构建指标体现;2)数据分析+验证迭代 3.关注的指标1)总体运营指标:1.1.活跃用户数、新增用户数1.2.总订单量、访问到下单转化率1.3.成交金额(GMV)、销售金额、客单价1.4.销售毛利、毛利率 2)流量指标:2.1.新增用户数、页面访问数2.2.用户获取成本2.3.跳出率、页面访问时长、人均页面访问数2.4.注册会员数、活跃会员数、活跃会员率、会员平均购买次数、会员留存率 3)销售指标:3.1.加入购物车次数、加入购物车买家数、加入购物车商品数、购物车支付转化率3.2.下单笔数、下单金额、下单买家数、浏览下单转化率3.3.支付金额、支付买家数、支付商品数、浏览-支付买家转化率、下单-支付买家转化率3.4.交易成功/失败订单数、交易成功/失败金额、交易成功/失败买家数、交易成功/失败商品数、退款订单数量、退款金额、退款率 4)客户价值指标:4.1.累积购买客户数、客单价4.2.新客户数量、新客户获取成本、新客户客单价4.3.消费频率、最近一次购买时间、消费金额、重复购买率 5)市场营销活动指标:5.1.新增访问人数、新增注册人数、总访问次数5.2.订单数量、下单转化率5.3.ROI(投资回报率) 4.如何分析1)渠道分析顺序:曝光–>点击–>下载–>注册–>付费核心数据是有效注册数,其次是付费用户数,最后是arpu值 2)用户运营:2.1.根据活跃度:新增活跃用户、活跃老用户、沉默用户、流失用户…2.2.根据商品偏好:美妆类、母婴类、零食类、电子产品类、书籍类…2.3.根据消费能力:普通会员、黄金会员、白金会员、钻石会员…2.4.根据消费频率:每周一次以上、每月1-2次、三个月2次…

September 9, 2019 · 1 min · jiezi

Redis进阶应用RedisLua脚本实现复合操作

一、引言Redis是高性能的key-value数据库,在很大程度克服了memcached这类key/value存储的不足,在部分场景下,是对关系数据库的良好补充。得益于超高性能和丰富的数据结构,Redis已成为当前架构设计中的首选key-value存储系统。 虽然Redis官网上提供了200多个命令,但做程序设计时还是避免不了为了实现一小步业务逻辑而多次调用Redis的情况。 以compare and set场景为例。如果使用Redis原生命令,需要从Redis中获取这个key,然后提取其中的值进行比对:如果相等就不做处理;如果不相等或者key不存在则将key设置成目标值。仅仅一个单点的compare and set操作就需要与Redis通讯两次。 此外,这种分散操作无法利用Redis的原子特性,占用多次网络IO。 今天我们就来探讨一下如何优雅地应对上述场景。 二、Redis与Lua在介绍Lua之前,我们需要先对这个语言有个初步了解。Lua 是一个小巧的脚本语言,几乎可以运行在所有操作系统和平台上。我们一般不会用Lua处理特别复杂的事务,因此只需了解一些lua的基本语法即可。 Redis问世之后,其开发者也意识到了开篇提到的问题,因此Redis从2.6版本开始支持Lua脚本。新版本的Redis还支持Lua Script debug,感兴趣的小伙伴可以去官网的Documentation中找到对应介绍和QuickStart。 有了Lua脚本之后,使用Redis程序时便能够在以下方面实现显著提升: 减少网络开销:本来N次网络请求的操作,可以用一个请求完成。原先N次请求的逻辑放在Redis服务器上完成,减少了网络往返时延;原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。这是一个重要特性,一定要拿小本本记好。至于为什么是一个原子操作,我们以后再分析;复用:客户端发送的脚本会永久存储在Redis中。这样其他客户端就可以复用这一脚本,而不需要使用代码完成同样的逻辑。所以现在流传一句话:要想学好Redis,必会Lua Script。 三、通过Lua脚本实现compare and set接下来我们就实现一个简单的compare and set,并通过这个例子感受一下Lua脚本给Redis使用带来的全新体验。 首先看一下如何让Redis执行Lua脚本。 3.1 Redis的EVALRedis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个Lua函数。numkeys: 用于指定键名参数的个数。key [key ...]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的Redis键(key)。在Lua中,这些键名参数可以通过全局变量 KEYS 数组,用1为基址的形式访问( KEYS[1] ,KEYS[2],依次类推)。arg [arg ...]: 附加参数,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。这里借用一下官网的例子。 上述脚本直接返回了入参。 eval为Redis关键字;第一个引号中的内容就是Lua脚本;2为参数个数;key1和key2是KEYS[1]、KEYS[2]的入参;first和second是ARGV[1],ARGV[2]的入参。大家可以简单地将KEYS[1],KEYS[2], ARGV[1],ARGV[2]理解为占位符。 3.2 执行脚本文件和缓存脚本如果只能在命令行中写脚本执行,遇到复杂的脚本程序岂不是会抓狂? 下面我们来看一下,如何让Redis执行Lua脚本文件,同时也验证一下lua脚本的复用特性(以后我们再也不需要定期批量删除某些符合特定规则的key了)。 Redis 127.0.0.1:6379> SCRIPT LOAD scriptRedis 127.0.0.1:6379> EVALSHA sha1 numkeys key [key ...] arg [arg ...]Redis提供了一个SCRIPTLOAD命令,命令后面的script即为Lua脚本。命令将脚本script添加到脚本缓存中,但并不立即执行这个脚本。执行命令后,Redis会返回一个SHA1串,第二个EVALSHA命令即可执行。 ...

August 7, 2019 · 1 min · jiezi

Kong网关部署

Kong是一个使用了lua-nginx-module运行在Nginx之上的Lua应用。Kong是一个成熟的API网关解决方案。API 网关,即API Gateway,是大型分布式系统中,为了保护内部服务而设计的一道屏障,可以提供高性能、高可用的 API托管服务,从而帮助服务的开发者便捷地对外提供服务,而不用考虑安全控制、流量控制、审计日志等问题,统一在网关层将安全认证,流量控制,审计日志,黑白名单等实现。网关的下一层,是内部服务,内部服务只需开发和关注具体业务相关的实现。网关可以提供API发布、管理、维护等主要功能。开发者只需要简单的配置操作即可把自己开发的服务发布出去,同时置于网关的保护之下。 参考文档:https://konghq.com/ (kong官网) https://www.pocketdigi.com/bo...https://www.postgresql.org/ (postgresql官网)http://www.postgres.cn/index....环境:环境:Centos7配置:2c4g权限:root一、安装PostgreSQL注意:请勿使用"yum install kong-community-edition"安装Kong,必须指定版本号!"yum install kong-community-edition-0.14.1.*.noarch.rpm --nogpgcheck"1、配置yum源# 配置完yum库之后卸载之前安装的Postgresqlyum erase postgresql*# 删除遗留的数据rm -rf /var/lib/pgsql2、安装下载RPM(PostgreSQL YUM源),找到对应的版本 CentOS 7 - x86_64 # 安装yum源yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm# 安装PostgreSQLyum install postgresql96-server postgresql96-contrib3、初始化数据库# 初始化数据库/usr/pgsql-9.6/bin/postgresql96-setup initdb4、PostgreSQL服务控制# PostgreSQL 使用systemctl作为服务托管service postgresql-9.6 start/stop/restart/reload# 或是systemctl start/stop/restart/status postgresql-9.6# 设置开机自启systemctl enable postgresql-9.65、卸载(顺便提供卸载PostgreSQL的命令)# 卸载PostgreSQLyum erase postgresql966、修改密码PostgreSQL数据库默认会创建一个Linux系统用户postgres,通过passwd命令可以设置密码。 # 创建postgres数据库账号su postgrespsqlALTER USER postgres WITH PASSWORD '123456';\qsu root7、设置远程控制7.1 修改vi /var/lib/pgsql/9.6/data/postgresql.conf文件,配置可以远程访问(正式环境按照白名单正确配置)将listen_addresses前的#去掉,并将 listen_addresses = 'localhost' 改成 listen_addresses = '*'; ...

July 14, 2019 · 2 min · jiezi

移动端orm框架性能测评

移动端orm框架性能测评flutter_orm_plugin 发布以来,不少团队试用了,我发现大家对这类数据库相关的库,第一反应就是性能如何,之前确实没做太多行业对比,最近觉得还是有必要做一下性能测试,给大家一个交代的。 在ios端,业界比较常用的orm框架应该是苹果官方推出的coredata,还有就是realm了。在android端orm框架我挑了三个比较常用的,greendao,realm和activeandroid。我会用flutter_orm_plugin跟上面提到的ios和android端orm框架做对比。 下面我会分别给出测试用例,测试代码,还有最终数据比较的结果。 测试用例测试用例我列了以下这些 10000次插入数据使用批量接口10000次插入数据10000次读取数据10000次修改数据使用批量接口10000次修改数据10000次删除数据使用批量接口10000次删除数据为什么会有普通插入数据和使用批量接口插入数据的区别,大部分orm框架都会对批量操作有一定的优化,所以需要对批量操作进行测试,但是在平时使用,不一定都能用上批量接口(例如多次数据操作不在同一代码块,或者在不同的模块中都要操作数据),所以我们会分别对普通操作和批量操作进行测试。 android 测试代码首先我们给出flutter_orm_plugin 的测试代码,由于不想因为flutter和原生channel通讯产生误差,我们直接用Luakit来写lua代码做测试(greendao、realm、activeandroid、coredata都不涉及flutter和原生channel通讯),flutter_orm_plugin其实底层就是luakit的orm框架,这个不影响测试准确性。 循环插入Luakit定义orm模型结构并做10000次插入,下面的代码是ios和android通用的。 local Student = { __dbname__ = "test.db", __tablename__ = "Student", studentId = {"TextField",{primary_key = true}}, name = {"TextField",{}}, claName = {"TextField",{}}, teacherName = {"TextField",{}}, score = {"RealField",{}}, } local params = { name = "Student", args = Student, } Table.addTableInfo(params,function () local studentTable = Table("Student”) for i=1,10000 do local s = { studentId = "studentId"..i, name = "name"..i, claName = "claName"..i, teacherName = "teacherName"..i, score = 90, } studentTable(s):save() end end)activeandroid定义orm模型结构并做10000次插入 ...

July 10, 2019 · 7 min · jiezi

lua-牛牛算法

最近在写棋牌游戏,写了一个牛牛的算法,在这里分享给大家! 部分代码参考了网上的! main.lua#!/usr/local/bin/lualocal card = require("card")local bit = require("bit")local cardBuffer = card.RandCardList()local cards1 = {}local cards2 = {}local cards3 = {}local cards4 = {}for i = 1, 20, 1 do local cardColor = bit.band(cardBuffer[i], 0xF0) / 16 + 1 local cardValue = bit.band(cardBuffer[i], 0x0F) local cardCount = card.getCountByValue(cardValue) local cardInfo = { card_value = cardValue, card_color = cardColor, card_count = cardCount } if i <= 5 then cards1[i] = cardInfo elseif i > 5 and i <= 10 then cards2[i - 5] = cardInfo elseif i > 10 and i <= 15 then cards3[i - 10] = cardInfo elseif i > 15 and i <= 20 then cards4[i - 15] = cardInfo elseif i > 20 and i <= 25 then cards5[i - 20] = cardInfo endendprint("庄家牌:", card.getCardNameByCards(cards1))print("庄家牛牛类型:", card.getCardTypeNameByType(card.getTypeByCards(cards1)))print("------------------------------------")print("闲1牌:", card.getCardNameByCards(cards2))print("闲1牛牛类型:", card.getCardTypeNameByType(card.getTypeByCards(cards2)))print("------------------------------------")print("闲2牌:", card.getCardNameByCards(cards3))print("闲2牛牛类型:", card.getCardTypeNameByType(card.getTypeByCards(cards3)))print("------------------------------------")print("闲3牌:", card.getCardNameByCards(cards4))print("闲3牛牛类型:", card.getCardTypeNameByType(card.getTypeByCards(cards4)))print("------------------------------------")print("闲4牌:", card.getCardNameByCards(cards5))print("闲4牛牛类型:", card.getCardTypeNameByType(card.getTypeByCards(cards5)))card.luacard = {}-- 花色CardColor = { Spade = 4, --黑桃 Heart = 3, --红桃 Plum = 2, --梅花 Block = 1, --方块}-- 牌型CardType = { NOT_NIU = 0, --没牛 NIU_1 = 1, --牛一 NIU_2 = 2, --牛二 NIU_3 = 3, --牛三 NIU_4 = 4, --牛四 NIU_5 = 5, --牛五 NIU_6 = 6, --牛六 NIU_7 = 7, --牛七 NIU_8 = 8, --牛八 NIU_9 = 9, --牛九 NIU_NIU = 10, --牛牛 SILVER_NIU = 11, --银牛 GOLD_NIU = 12, --金牛 BOMB = 13, --炸弹 SMALL_NIU = 14, --五小牛}-- 所有的扑克牌CardValue = { card_A = 1, card_2 = 2, card_3 = 3, card_4 = 4, card_5 = 5, card_6 = 6, card_7 = 7, card_8 = 8, card_9 = 9, card_10 = 10, card_J = 11, card_Q = 12, card_K = 13,}--扑克数据CardData = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D,}function card.getCountByValue(value) if value > 10 then return 10 else return value endendfunction card.getCardNameByCard(Card) local string = "" if Card.card_color == 4 then string = string .. "黑桃" elseif Card.card_color == 3 then string = string .. "红桃" elseif Card.card_color == 2 then string = string .. "梅花" elseif Card.card_color == 1 then string = string .. "方块" else string = "ERROR" end if Card.card_value == 13 then string = string .. "K" elseif Card.card_value == 12 then string = string .. "Q" elseif Card.card_value == 11 then string = string .. "J" else string = string .. Card.card_value end return stringendfunction card.getCardNameByCards(Cards) local string = "" for i = 1, #Cards do string = string .. card.getCardNameByCard(Cards[i]) end return stringend-- 洗牌function card.RandCardList() math.randomseed(os.time()) for i = 1, #CardData do local ranOne = math.random(1, #CardData + 1 - i) CardData[ranOne], CardData[#CardData + 1 - i] = CardData[#CardData + 1 - i], CardData[ranOne] end local cardBuffer = card.deepCopy(CardData); return cardBuffer;endfunction card.deepCopy(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[card.deepCopy(orig_key)] = card.deepCopy(orig_value) end setmetatable(copy, card.deepCopy(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copyendfunction card.compByCardsValue(a, b) if a.card_value < b.card_value then return true end if a.card_value > b.card_value then return false end return a.card_color < b.card_colorendfunction card.sortByCardsValue(cards) table.sort(cards, card.compByCardsValue);endfunction card.is_small_niu(cards) local sum = 0 for i = 1, #cards do sum = sum + cards[i].card_count end if sum <= 10 then return true else return false endendfunction card.is_bomb(cards) if cards[1].card_value == cards[4].card_value then return true elseif cards[2].card_value == cards[5].card_value then return true else return false endendfunction card.is_gold_niu(cards) if cards[1].card_value > 10 then return true else return false endendfunction card.is_silver_niu(cards) if cards[2].card_value > 10 and cards[1].card_value == 10 then return true else return false endendfunction card.getNiuByCards(cards) local lave = 0 --余数 for i = 1, #cards do lave = lave + cards[i].card_count end lave = lave % 10 for i = 1, #cards - 1 do for j = i + 1, #cards do if (cards[i].card_count + cards[j].card_count) % 10 == lave then if lave == 0 then return 10 else return lave end end end end return 0endfunction card.getTypeByCards(cards) card.sortByCardsValue(cards) local cardType = CardType.NOT_NIU if card.is_small_niu(cards) then cardType = CardType.SMALL_NIU return cardType end if card.is_bomb(cards) then cardType = CardType.BOMB return cardType end if card.is_gold_niu(cards) then cardType = CardType.GOLD_NIU return cardType end if card.is_silver_niu(cards) then cardType = CardType.SILVER_NIU return cardType end cardType = card.getNiuByCards(cards) return cardTypeendfunction card.getCardTypeNameByType(CardType) if CardType == 0 then return "没牛" end if CardType == 1 then return "牛一" end if CardType == 2 then return "牛二" end if CardType == 3 then return "牛三" end if CardType == 4 then return "牛四" end if CardType == 5 then return "牛五" end if CardType == 6 then return "牛六" end if CardType == 7 then return "牛七" end if CardType == 8 then return "牛八" end if CardType == 9 then return "牛九" end if CardType == 10 then return "牛牛" end if CardType == 11 then return "银牛" end if CardType == 12 then return "金牛" end if CardType == 13 then return "炸弹" end if CardType == 14 then return "五小牛" end return "异常牌型"endfunction card.bankerIsWin(banker_Cards, other_Cards) local banker_Cards_Type = card.getTypeByCards(banker_Cards) local other_Cards_Type = card.getTypeByCards(other_Cards) if banker_Cards_Type ~= other_Cards_Type then return banker_Cards_Type > other_Cards_Type end if banker_Cards_Type == CardType.SMALL_NIU then return true end if banker_Cards_Type == CardType.BOMB then return banker_Cards[3].card_value > other_Cards[3].card_value end if banker_Cards_Type == CardType.GOLD_NIU then return card.compByCardsValue(other_Cards[5], banker_Cards[5]) end if banker_Cards_Type == CardType.SILVER_NIU then return card.compByCardsValue(other_Cards[5], banker_Cards[5]) end if banker_Cards_Type == CardType.NIU_NIU then return card.compByCardsValue(other_Cards[5], banker_Cards[5]) end if banker_Cards_Type == CardType.NOT_NIU then return card.compByCardsValue(other_Cards[5], banker_Cards[5]) end return trueendreturn card

July 8, 2019 · 5 min · jiezi

Lua-Web快速开发指南10-利用MQ实现异步任务订阅发布消息队列

本章节我们将学习如何使用MQ库. MQ库简介MQ库实现了各类消息代理中间件(Message Broker)的连接协议, 目前支持:redis、mqtt、stomp协议. MQ库基于上述协议实现了: 生产者 -> 消费者与订阅 -> 发布模型, 可以在不依赖其它服务的情况下独立完成任务. API介绍cf框架提供了多种MQ的封装, 当我们需要使用的时候需要根据实际的协议进行选择: -- local MQ = require "MQ.mqtt"-- local MQ = require "MQ.redis"-- local MQ = require "MQ.stomp"MQ:new(opt)此方法将会创建一个的MQ对象实例. opt是一个table类型的参数, 可以传递如下值: host - 字符串类型, 消息队列的域名或者IP地址.port - int类型, 消息队列监听的端口.auth/db - 字符串类型, 仅在redis协议下用作登录认证或者db选择(没有可以不填写).username/password - 字符串类型, 仅在stomp/mqtt协议下用作登录认证(没有可以不填写).vhost - 字符串类型, 仅在使用某些特定消息队列server的时候填写(例如:rabbit).keepalive - int类型, 仅在使用mqtt的时候用来出发客户端主动发出心跳包的时间.以redis broker为示例: local MQ = require "MQ.redis"local mq = MQ:new { host = "localhost", port = 6379, -- db = 0, -- auth = "123456789",}MQ:on(pattern, function)此方法用来订阅一个指定pattern. 当broker将消息传递到cf后, function将会被调用. ...

June 25, 2019 · 5 min · jiezi

Lua-Web快速开发指南9-使用cf内置的异步库

API 介绍cf框架提供内置的异步库cf, 需要使用的时候我们必须先导入API: local cf = require "cf". 定时器与循环定时器cf库内置了一些定时器方法, 这些方法为开发者提供了对时间事件的控制能力. cf.timeout、cf.at、cf.sleep. cf.sleep方法是一个阻塞的定时器, 只有一个参数用来设置当前协程的休眠时间并且没有返回值. 此方法的行为(语义)取决于用户传入的参数: 当时间参数大于0的时候, 当前协程会暂停指定的时间且让出执行权. 当指定的时间超时后函数将会返回继续执行下面的代码.当时间参数等于0的时候, 当前协程会暂停并且让出执行权. 当其它协程执行完毕(让出)后立刻返回.当时间参数小于0或者非number类型的时候, 此方法将立刻返回.cf.timeout与cf.at不会阻塞当前协程执行流程. 目前虽然暴露给开发者使用, 但真正的使用场景都仅限于在需要长连接业务内. cf.timeout与cf.at都会返回一个timer对象, 开发者可以在任何时候使用timer对象的stop方法停止定时器. cf.timeout与cf.at的参数如下: 第一个参数是一个指定的时间, 其在现实中的时间比例为1:1.第二个参数是一个回调函数, 当时间事件触发后将会为用户执行用户定义的回调函数.记住: cf.timeout是一次性定时器, 回调函数被触发之后将会自动停止运行. 而cf.at如果不使用stop方法停止则会一直重复执行. 协程的使用、暂停、唤醒cf库提供了协程的操作方法. 此协程与Lua的原生协程有些许不同, cf基于原生协程的基础上由框架管理生命周期. 需要异步执行一个函数可以使用cf.fork创建一个由cf调度的协程, 此方法会返回一个协程对象. 这个协程对象可以在它让出的时候用来主动唤醒. cf.fork方法的第一个参数func为function类型, 从第二个参数开始的参数将会作为func的参数(一般情况下我们会利用upvalue而不会显示传递参数). 需要暂停一个cf创建的协程可以使用cf.wait方法. 此方法没有参数, 但如果调用此方法的协程不是由cf创建或不是main协程则会出错. cf.wakeup方法用于唤醒由cf.wait暂停的协程. cf.wait方法的返回值由cf.wakeup的行为决定, 当唤醒的是不存在的协程或唤醒正在执行的协程将会出错. cf.wakeup方法的第一个参数是一个协程对象, 协程对象之后的所有参数将会返回给cf.wait进行接收. 需要获取当前协程对象的时候在这个协程执行流程之间使用cf.self方法获取, cf.self的作用与内置库coroutine.running方法相同. 它返回一个协程对象与一个boolean值. 当协程对象为主(main)协程时则bolean为true, 否则为false. 更多详细的API介绍更多使用介绍请参考cf库的文档. 开始实践1. 随机生成三个定时器并且输出时间.在本示例中! 我们首先修改随机数生成器种子, 随机从0~1中间取一个随机数作为定时器的时间. 然后启动一个循环开始生成3个定时器. -- main.lualocal cf = require "cf"math.randomseed(os.time()) -- 设置随机数种子for index = 1, 3 do local time = math.random() cf.timeout(time, function() print("第"..index.."个定时器的时间为:"..time) end)end由于是随机生成的时间, 所以我们在函数内部使用print方法将当前定时器的运行信息打印出来(第几个创建的定时器与定时器时间). ...

June 24, 2019 · 2 min · jiezi

Redis-Lua-接口限流最佳实践策略

1.应用场景我们开发的接口服务系统很多都具有抗高并发,保证高可用的特性。现实条件下,随着流量的不断增加,在经费、硬件和资源受限的情况下,我们就需要为我们的系统服务制定有效的限流、分流策略来保护我们的系统了。 2.算法简介和示例说明业界比较流行的限流算法有漏桶算法和令牌桶算法。 2.1漏桶算法漏桶(Leaky Bucket)算法的实现思路比较简单,水(请求)先流入到桶中,然后桶以一定的速度出水(接口有响应速率),当水流过大时(访问频率超过设置的阈值),系统服务就会拒绝请求。强行限制系统单位时间内访问的请求量。漏桶算法示意图如下:漏桶算法有两个关键变量:桶的大小和出水速率,他们共同决定了单位时间内系统能接收的最大请求量。因为漏桶算法中桶的大小和出水速率是固定的参数。不能使流突发到端口,对存在突发特性的流量缺乏效率,什么意思呢?我们后边会使用使用php实现一个漏桶demo,并对测试结果做详细说明。github源码地址是:漏桶算法demo 2.2令牌桶算法令牌桶(Token Bucket)和漏桶(Leaky Bucket)使用方向相反的算法,这种算法更加容易理解。随着时间的流逝,系统会按照恒定1/QPS(如果QPS=1000,则时间间隔是1ms)向桶中添加Token(想象和漏洞漏水相反,有个水龙头在不断的加水)。如果桶已经满了就不会添加了,请求到来时会尝试从桶中拿一个Token,如果拿不到Token,就阻塞或者拒绝服务,待下次有令牌时再去拿令牌。令牌桶的算法如下图所示例:令牌桶的好处是显而易见的,我们可以通过提高放入桶中令牌的速率,改变请求的限制速度。令牌桶一般会定时的向桶中添加令牌(例如每隔10ms向桶中添加一枚令牌)。我们会使用Go语言实现一个令牌桶demo,为了达到兼容分布式并发场景,我们会对令牌桶的demo做改进说明,我们在添加令牌时采用一种变种算法:等请求到达时根据令牌放入桶中的速率实时计算应该放入桶中令牌的数量。github源码地址是:令牌桶算法demo 2.3示例说明我们模拟实现的功能是限制一个公司下对某一个接口的访问频次,示例中是限制公司org1的员工列表接口/user/list在1s内能被外部访问100次。 3.示例源码和压测结果3.1 php实现漏桶算法Redis中设置接口限制1s内访问100次的hash: hmset org1/user/list expire 1 limitReq 100我们使用Predis连接redis进行操作,模拟接口比较简单,我们只获取两个参数,org和pathInfo,RateLimit类中相关方法是: <?php/** * Description: 漏桶限流 * User: guozhaoran<guozhaoran@cmcm.com> * Date: 2019-06-13 */class RateLimit{ private $conn = null; //redis连接 private $org = ''; //公司标识 private $pathInfo = ''; //接口路径信息 /** * RateLimit constructor. * @param $org * @param $pathInfo * @param $expire * @param $limitReq */ public function __construct($org, $pathInfo) { $this->conn = $this->getRedisConn(); $this->org = $org; $this->pathInfo = $pathInfo; } //......此处省略getLuaScript方法 /** * 获取redis连接 * @return \Predis\Client */ private function getRedisConn() { require_once('vendor/autoload.php'); $conn = new Predis\Client(['host' => '127.0.0.1', 'port' => 6379,]); return $conn; } //......此处省略isActionAllowed方法}下边我们看看Lua脚本的设计: ...

June 23, 2019 · 4 min · jiezi

为skynet移植一个luawebsocke库

简介目前大部分游戏、移动互联网、H5客户端主要由JavaScript、Lua、C#、C++等语言进行逻辑开发, 其主要通讯方案便是基于HTTP协议的接口请求与Websocket的推送方案. 起因skynet内部实现了一套同步非阻塞socket库, 并且提供了TCP通讯方案进行数据流分割. 所谓的TCP数据流分割. 就是根据一定方式读取数据的一种流程. 最为常见的数据分割方案应该是: 2字节头部 + 数据载荷. 另一种通用方案是将头部扩展为4字节, 这样在头部信息中可以包含协议版本或者消息类型还可以进行平滑的进行协议升级扩展. 这些方案一般用于定制C/S网络协议. 绝大多数场景中并没有必须使用到这个场景, 且维护一套这样的协议也是需要占用开发周期的.本人也使用过国内的开发者基于skynet编写的websockket开发库. 就使用上来说效果不是很理想, 且期间遇到的一系列问题也需要自己实际定制化后才能解决. 刚好近期由于正在为开发的Lua Web框架编写Websocket使用教程, 那么干脆趁这个机会为skynet移植一套专用的websocket库. 编写完成后, 我将它随意的命名为: skynet-lua-websocket. 开始移植工作1. 握手流程skynet要使用Websocket协议进行通讯需要实现HTTP/1.1版本中101响应方法. skyent.httpd库可以完成HTTP协议的解析工作, 但是我们并没有使用到它. 究其原因是因为websocket实现交互并不复杂且所以无需依赖其它应用层协议库的实现. HTTP本身就是一个基于文本的交互协议, 我们可以通过文本分割方案来完成它. 当连接到来时我们需要定义一个方法来处理握手协议交互(do_handshak). 在握手期间我们需要等待客户端发送有效的HTTP请求数据(协议、方法、版本、头部等等). 在握手期间我们不能忘记给它加上一个超时限制(set_timeout), 这个限制需要然客户端在指定时间范围内完成握手. 否则, 只能断开连接来节省服务器资源开销. 当HTTP请求数据接收完毕后, 我们需要对头部信息进行简单的验证. 这个验证过程并不会很复杂, 因为我们只需要知道头部信息是否完整有效即可. 在验证完成与通过的时候, 我们需要返回一个协议升级成功的101回应来通知客户端, 可以使用websocket规定的协议进行通讯并且开始监听链接是否有数据即可. 2. 消息交互Websocket协议规范中定义了一些常用的消息(控制帧). 目前为止, 我们也仅需要使用到这些消息: text、binary、ping、pong、close. text/binary可以分为一种客户端请求消息, 它定义了客户端发送到服务端的数据是什么类型. 这通常在项目开发初期已经约定好传输协议, 所以无需过多考虑. ping/pong通常是成对出现的; 它一般用作心跳检查(虽然没有人这样做)与交互测试工作. close一般主动推送关闭消息, 一般情况下接收到这样的消息的处理方式为关闭连接. 3. 事件处理事件处理方式就仿照JavaScript设计定义了4种回调函数类型(on_open、 on_message、 on_error、on_close), 这样能简化代码编写难度. 在每个客户端连接到来的时候为用户初始化ctor方法并为其注入ws对象用于与客户端进行通讯(send、close). 当客户端连接建立完成后会在应用层触发on_open方法, 让开发者此时做一些相关的初始化的操作. 期间与客户端连接保持的期间内定义了on_message方法用于接收客户端的数据, 对需要回应的数据可以使用self.ws:send方法进行消息回应. ...

June 22, 2019 · 1 min · jiezi

Lua-Web快速开发指南8-利用httpd提供Websocket服务

Websocket的技术背景WebSocket是一种在单个TCP连接上进行全双工通信的协议, WebSocket通信协议于2011年被IETF定为标准RFC 6455并由RFC7936补充规范. WebSocket使得客户端和服务器之间的数据交换变得更加简单, 使用WebSocket的API只需要完成一次握手就直接可以创建持久性的连接并进行双向数据传输. WebSocket支持的客户端不仅限于浏览器(Web应用), 在现今应用市场内的众多App客户端的长连接推送服务都有一大部分是基于WebSocket协议来实现交互的. Websocket由于使用HTTP协议升级而来, 在协议交互初期需要根据正常HTTP协议交互流程. 因此, Websocket也很容易建立在SSL数据加密技术的基础上进行通信. 协议WebSocket与HTTP协议实现类似但也略有不同. 前面提到: WebSocket协议在进行交互之前需要进行握手, 握手协议的交互就是利用HTTP协议升级而来. 众所周知, HTTP协议是一种无状态的协议. 对于这种建立在请求->回应模式之上的连接, 即使在HTTP/1.1的规范上实现了Keep-alive也避免不了这个问题. 所以, Websocket通过HTTP/1.1协议的101状态码进行协议升级协商, 在服务器支持协议升级的条件下将回应升级请求来完成HTTP->TCP的协议升级. 原理客户端将在经过TCP3次握手之后发送一次HTTP升级连接请求, 请求中不仅包含HTTP交互所需要的头部信息, 同时也会包含Websocket交互所独有的加密信息. 当服务端在接受到客户端的协议升级请求的时候, 各类Web服务实现的实际情况, 对其中的请求版本、加密信息、协议升级详情进行判断. 错误(无效)的信息将会被拒绝. 在两端确认完成交互之后, 双方交互的协议将会从抛弃原有的HTTP协议转而使用Websocket特有协议交互方式. 协议规范可以参考RFC文档. 优势在需要消息推送、连接保持、交互效率等要求下, 两种协议的转变将会带来交互方式的不同. 首先, Websocket协议使用头部压缩技术将头部压缩成2-10字节大小并且包含数据载荷长度, 这显著减少了网络交互的开销并且确保信息数据完整性. 如果假设在一个稳定(可能)的网络环境下将尽可能的减少连接建立开销、身份验证等带来的网络开销, 同时还能拥有比HTTP协议更方便的数据包解析方式. 其次, 由于基于Websocket的协议的在请求->回应上是双向的, 所以不会出现多个请求的阻塞连接的情况. 这也极大程度上减少了正常请求延迟的问题. 最后, Websocket还能给予开发者更多的连接管控能力: 连接超时、心跳判断等. 在合理的连接管理规划下, 这可提供使用者更优质的开发方案. APIcf框架中的httpd库内置了Websocket路由, 提供了上述Websocket连接管理能力. Websocket路由需要开发者提供一个lua版的class对象来抽象路由处理的过程, 这样的抽象能简化代码编写难度. lua classclass 意译为'类'. 是对'对象'的一种抽象描述, 多用于各种面相对象编程语言中. lua没有原生的class类型, 但是提供了基本构建的元方法. cf为了方便描述内置对象与内置库封装, 使用lua table的相关元方法建立了最基本的class模型. 几乎大部分内置库都依赖cf的class库. 同时为了简化class的学习成本, 去除了class原本拥有的'多重继承'概念. 将其仅作为类定义, 用于完成从class->object的初始化工作. ...

June 18, 2019 · 2 min · jiezi

lua-web快速开发指南7-高效的接口调用-httpc库

httpc库基于cf框架都内部实现的socket编写的http client库. httpc库内置SSL支持, 在不使用代理的情况下就可以请求第三方接口. httpc支持header、args、body、timeout请求设置, 完美支持各种httpc调用方式. API介绍httpc库使用前需要手动导入httpc库: local httpc = require "httpc". httpc.get(domain, HEADER, ARGS, TIMEOUT)调用get方法将会对domain发起一次HTTP GET请求. domain是一个符合URL定义规范的字符串; HEADER是一个key-value数组, 一般用于添加自定义头部; ARGS为请求参数的key-value数组, 对于GET方法将会自动格式化为:args[n][1]=args[n][2]&args[n+1][1]=args[n+1][2]; TIMEOUT为httpc请求的最大超时时间; httpc.post(domain, HEADER, BODY, TIMEOUT)调用post方法将会对domain发起一次HTTP POST请求, 此方法的content-type会被设置为:application/x-www-form-urlencoded. domain是一个符合URL定义规范的字符串; HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; BODY是一个key-value数组, 对于POST方法将会自动格式化为:body[n][1]=body[n][2]&body[n+1][1]=body[n+1][2]; TIMEOUT为httpc请求的最大超时时间; httpc.json(domain, HEADER, JSON, TIMEOUT)json方法将会对domain发起一次http POST请求. 此方法的content-type会被设置为:application/json. HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; JSON必须是一个字符串类型; TIMEOUT为httpc请求的最大超时时间; httpc.file(domain, HEADER, FILES, TIMEOUT)file方法将会对domain发起一次http POST请求. HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置; FILES是一个key-value数组, 每个item包含: name(名称), filename(文件名), file(文件内容), type(文件类型)等属性. 文件类型可选. TIMEOUT为httpc请求的最大超时时间; httpc 返回值所有httpc请求接口均会有2个返回值: code, response. code为http协议状态码, response为回应body(字符串类型). ...

June 16, 2019 · 4 min · jiezi

lua-web快速开发指南6-CacheDB介绍

"数据库"与"缓存"的基本概念数据库与缓存是服务端开发人员的必学知识点. 数据库"数据库"是一种信息记录、存取的虚拟标记地点的集合统称. 比如现实生活中, 我们经常会用到文件柜、书桌等等数据存取容器. 在对容器进行数据存取的时候, 我们会为每一层打上一个标签表示一种分类项. 而这种在数据库中划分子分类形成了表的概念. 这就是我们通常所说的结构化数据库. 由于通常数据表之间可能会存在依赖关系, 某一(或者多)层通常可能会用于同一种用途. 这种用途将一层划分为索引表, 二层划分为分类表, 三层划分为数据表. 实现这种功能与依赖关系的数据库, 我们称之为: 关系型数据库. 它可以定义一套规范并且建立数据存取模型, 这样方便维护一整套结构化的数据信息. 每当我们需要对数据进行结构化操作(查询、增加、删除、修改)的时候, 需要在计算机中用一种通俗易懂的语言表达方式来进行助记. 这种结构化查询语言称之为SQL. 缓存我们通常将数据存储完毕后, 能通过指定或特定的一(多)种方式对数据进行操作. 在项目开发的初期, 这并没有太大的问题. 但是随着数据量的不断增大, 在数据库的内存中已经放不下这么多数据. 我们的数据逐渐无法被加载到内存中: 只会在使用的时候才会进行(随机)读取. 而这会加大磁盘I/O. 我们知道通常磁盘的读写速度基本上会比内存读写慢几个数量级(即使是SSD), 大量请求可能瞬间将磁盘IO占满并出现数据库的CPU利用率低、内存频繁进行修改/置换等问题. 为了解决这些问题, 出现了很多解决方案: 读、写分离、分表分库等等. 虽然有了这些方案, 但是也同样回引来新的问题: 主从同步、分布式事务等问题. "缓存"则是近十年兴起的概念, 它的本质是一份数据结构化存储在内存中的副本. 高级的缓存我们也可以将其称之为内存数据库或NOSQL(非关系型)数据库. "缓存"也是一种"另类"解决数据库问题点一种手段! 它通过丰富的数据结构扩展了数据模型的组合能力, 通过简单的使用方法与高效的连接方式提供更好数据操作方式. "缓存"将查询、更新较为频繁的热数据组成一个集合加载进内存中, 较少使用的冷数据序列化到磁盘内部. 高效利用内存的同时, 根据变化的情况合理更新、删除缓存. 这样的方式配合数据库都读、写分离与数据分区将数据合理的从一个数据集副本分散到多个数据集副本, 有效的减少性能问题点产生并且提升了整个业务系统的横向扩展能. DB库DB库是cf框架封装自MySQL 4.1协议实现的客户端连接库, 提供MySQL断线重连、SQL重试、连接池等高级特性. CacheCache库是cf封装自Redis 2.0协议实现的客户端连接库, 提供Redis断线重连、命令重试、连接池等高级特性. API学习1. DB API在使用下面的API之前, 请先确保已经导入库: local DB = require "DB". 1.1 DB:new(opts)opts表的参数决定如何连接到MySQL, 表属性如下: ...

June 16, 2019 · 2 min · jiezi

lua-web快速开发指南5-利用template库构建httpd模板引擎

介绍template模板引擎是为了使用户界面与业务数据(内容)分离而产生的, 其本身并不是一种深奥的技术. template模板引擎首先会将合法的模板编译为lua函数, 然后将模板文件和数据通过模板引擎生成一份HTML代码. cf的admin库整使使用了template来构建服务端渲染页面, 并利用单页面+iframe模式快速完成lua后台开发. 1. template基础语法在真正使用之前, 我们先来学习一下template常见的一些基本语法: {{ lua expression }} - lua expression是一段lua表达式; 作用为输出表达式的结果, 一些特殊符号将会被转义;{* lua expression *} - lua expression是一段lua表达式; 作用为输出表达式的结果, 不会转义任何符号;{% lua code %} - 执行一段lua代码, 如: {% for i = x, y do %} ... {% end %};{# comments #}- comments仅作为注释, 不会包含在输出字符串内. 这段语法的作用类似lua内的--与--[[]];{(template)} - 导入其它模板文件; 同时支持传参: {(file.html, { message = "Hello, World" })};2. 转义字符& 将会转义为 &amp;< 将会转义为 &lt;> 将会转义为 &gt;" 将会转义为 &quot;' 将会转义为 &#39;/ 将会转义为 &#47;3. APItemplate.compile(html)参数html为字符串类型, 可以是:模板文件路径、 ...

June 14, 2019 · 2 min · jiezi

lua-web快速开发指南4-详细了解httpd库的作用

httpd库是基于HTTP 1.1协议实现而来, 内置了高性能的http协议解析器与urldecode解析库. httpd库默认情况下就能工作的很好, 但是在一些需求较为极端的场景还是需要微调一下参数. httpd常用的内置方法介绍1. httpd:timeout(number)设置每个连接到最大空闲(idle)连接等待时间, 超过这个数值httpd将主动断开连接. (默认值为:30秒) 2. httpd:max_path_size(number)设置Path的最大长度, 超过这个值httpd将会返回414. (默认值为: 1024) 3. httpd:max_header_size(number)设置Header最大长度, 超过这个值httpd将会返回431. (默认值为: 65535) 4. httpd:max_body_size(number)设置Body的最大长度, 超过这个值将会返回413. (默认为 1024 * 1024) 5. httpd:before(function)before方法决定API与USE路由回调在触发之前的行为, 默认情况下允许所有路由通过. before方法一般用来设置与修改用户验证路由行为(例如头部验证), 这提供了开发者基于before函数设计中间件的机会. 当开发者设置了function后(即是是一个空函数), 需要利用http库来决定行为. 6. httpd:group(type, prefix, handles)group方法提供了一种批量注册路由的方式, 为一组同一组路由提供简单便方便在注册方法. 第一个参数type为需要批量注册的路由类型; 初始化httpd对象后, 使用app.USE或app.API进行传值; 第二个参数prefix为string类型的头部; 例如:/api、/admin; 第三个参数为一组路由处理函数或处理类数组; 类型为: {route = '/login', class = class}; 注意: 此方法仅支持批量注册API与USE路由, 不可同时注册不同类型路由; 7. httpd:static(folder, ttl)listen方法用于告诉httpd对象监听指定端口. 第一个参数ip暂未被httpd使用(但是必须设置), 默认监听所有网卡的'0.0.0.0'地址与指定的端口号; backlog为用户最大连接等待队列, 合理的设置能减少连接被重置的情况(默认值为128). 8. httpd:run()在httpd库所有参数与路由设置完毕之后, 调用run方法开启监听模式. httpd的请求日志日志格式为: [年/月/日 时:分:秒] - [ip] - [x-real-ip] - [path] - [method] - [http code] - [request handle timeline] ...

June 14, 2019 · 1 min · jiezi

lua-web快速开发指南3-初识httpd库路由

本章假设您已经知道httpd server如何快速搭建, 并且知道cf的启动流程与运行流程, 知晓httpd如何创建与启动. 回顾上一章节-- script/main.lualocal httpd = require "httpd"local app = httpd:new("app")app:static("static", 30)app:listen("0.0.0.0", 8080)app:run()我们利用httpd内置库快速实现了一套httpd静态文件server, 其中包括静态文件目录指定与端口设置. 并且在启动server后可以在看到测试页面. 什么是"路由"与"路由表"?Web路由用于描述资源到处理函数之间的一个映射关系. Web路由表用于描述当前作用域下所有路由的一个集合. 如下所示: /userlogin -> function userlogin(content) ... end/userinfo -> function userinfo(content) ... end对于一个服务端开发者来说! 当接受到客户端的HTTP请求时, 服务端会将请求URL中的PATH进行分割, 然后开始寻找的PATH映射对应的回调处理函数. 当URL映射的回调处理函数被找到时, 将会为其注入整个http上下文并且根据处理函数的行为将返回值展现给资源访问者. 这就是基本的路由雏形. cf中的各种路由cf的httpd库利用这种机制, 为开发者提供了一整套完整的路由注册方法, 其中包括: 静态文件路由、API接口路由、USE页面路由、WebSocket路由. 静态文件路由我们在上一章节已经看到过, 其本质是根据需要读取指定文件而存在的. 这种路由一般有库编写者或者框架编写者实现. 而API接口路由、USE页面路由、Websocket路由则一般由开发自行指定, 这些路由一般都用来处理对应的业务逻辑. 下面我们就开始学习如何在cf中注册路由. 注册API与USE路由1. API路由API接口路由用于快速构建前、后端分离的web开发场景. 它提供了基于http协议提供了基础的前、后端通讯的解决方案, 是目前位置Web领域最为常见的开发模式. 而作为前、后端数据沟通的桥梁自然需要指定指定数据交互类型. 目前为止, API路由的content-type为"application/json", 数据交互格式仅支持: json. httpd库为开发者提供了app:api方法用来注册API路由, 第一个参数是一个字符串类型的资源路径, 第二个参数则是回调处理方法; 现在让我们在main.lua中, 添加我们刚刚学习到的api路由: -- main.lualocal json = require "json"app:api('/userinfo', function(content) return json.encode({ code = 200, user = { name = "CandyMi", age = 29, sex = "男", } })end)然后打开浏览器, 输入http://localhost:8080/userinfo. 我们就可以看到我们输出的接口数据了. ...

June 14, 2019 · 1 min · jiezi

Ace3魔兽世界插件开发之旅一-WelcomeHome

本文一步一步讲解如何通过Ace3开发框架构建一个WelcomeHome插件。文中大部分内容翻译自gamepedia,原文地址:https://wow.gamepedia.com/WelcomeHome_-_Your_first_Ace3_Addon。由于英文水平有限,有不对之处还望指正,谢谢! 准备工作目前魔兽的版本是8.1.5,魔兽插件都在<World of Warcraft_retail_InterfaceAddOns>这个目录下,所以我们先建一个目录:WelcomeHome,当WOW在AddOns目录下发现一个目录时,它会去找这个目录下跟目录同名的TOC文件,这个TOC文件包含了本插件所有文件的清单,WOW会使用这个文件来加载这个插件。下面是我们这个WelcomeHome.TOC文件的基本骨架: ## Interface: 81500## Version: 0.1## Title: Welcome Home## Author: xiaop## Notes: 炉石的时候显示欢迎信息.Core.lua这时候需要建一个空的Core.lua文件,用来存放插件的代码。现在我们先让它空着,这时候登录WOW在人物选择界面点击插件可以看到WelcomeHome这个插件,虽然它啥也干不了。 引入Ace3库要想让我们的插件具备具体功能,我们需要引入Ace3相关的库。Ace3使用了一个叫做“嵌入式库”的概念,它允许模块开发者在其他模块加载了相同库的时候不需要再复制一份代码。我们可以在 http://www.wowace.com/addons/ace3/files/这里下载最新的Ace3库,然后解压到插件目录的Libs目录下。本文需要用到以下的库: AceAddon-3.0AceDB-3.0AceConfig-3.0AceConsole-3.0AceEvent-3.0AceGUI-3.0CallbackHandler-1.0LibStub现在我们有了Ace3相关的库,但是WOW并不知道如何加载他们,我们需要一个embeds.xml文件来告诉WOW需要加载哪些文件,于是我们新建一个embeds.xml文件,内容如下: <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd"> <Script file="Libs\LibStub\LibStub.lua"/> <Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/> <Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/> <Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/> <Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/> <Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/> <Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/> <Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/></Ui>Script标签表示需要引入的lua文件,Include标签表示要引入的xml文件。需要注意的是LibStub必须先加载,因为其他库都依赖它。其他文件也需要注意使用顺序,原则就是被依赖的库需要先加载。现在我们更新一下TOC文件,在core.lua之前引入embeds.xml文件,以保证在core.lua执行之前所有的库已被加载并可以使用。 ## Interface: 81500## Version: 0.1## Title: Welcome Home## Author: xiaop## Notes: 炉石的时候显示欢迎信息.## SavedVariables: WelcomeHomeDB## OptionalDeps: Ace3## X-Embeds: Ace3embeds.xmlCore.luaHello World下面我们编辑一下Core.lua文件,加入Ace3最基本的结构: WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0")function WelcomeHome:OnInitialize() -- Called when the addon is loadedendfunction WelcomeHome:OnEnable() -- Called when the addon is enabledendfunction WelcomeHome:OnDisable() -- Called when the addon is disabledend第一行使用NewAddon方法创建一个AceAddon类的实例,因为我们会用到聊天窗口和斜杠命令,我们还混入了AceConsole类。接下来是三个重写的方法OnInitialize, OnEnable, 和 OnDisable。OnInitialize只会在UI加载的时候执行一次,后面两个分别在插件被启用和禁用的时候执行。下面我们假设插件正在加载,我们想在聊天窗口打印一句“Hello World”,只需要在OnEnable方法中添加一行代码: ...

June 14, 2019 · 4 min · jiezi

RedisLua实现分布式限流器

LastModified: 2019年6月14日10:37:39 主要是依靠 redis + lua 来实现限流器, 使用 lua 的原因是将多条命令合并在一起作为一个原子操作, 无需过多考虑并发. 计数器模式原理计数器算法是指在一段窗口时间内允许通过的固定数量的请求, 比如10次/秒, 500次/30秒. 如果设置的时间粒度越细, 那么限流会更平滑. 实现所使用的 Lua 脚本 -- 计数器限流-- 此处支持的最小单位时间是秒, 若将 expire 改成 pexpire 则可支持毫秒粒度.-- KEYS[1] string 限流的key-- ARGV[1] int 限流数-- ARGV[2] int 单位时间(秒)local cnt = tonumber(redis.call("incr", KEYS[1]))if (cnt == 1) then -- cnt 值为1说明之前不存在该值, 因此需要设置其过期时间 redis.call("expire", KEYS[1], tonumber(ARGV[2]))elseif (cnt > tonumber(ARGV[1])) then return -1end return cnt返回 -1 表示超过限流, 否则返回当前单位时间已通过的请求数key 可以但不限于以下的情况 ip + 接口user_id + 接口优点 ...

June 14, 2019 · 2 min · jiezi

告别单机linux面板的臃肿使用云端轻平台运维

时间进入2019年,linux服务器市场现在使用的还是各个企业或有能力的站长,对于linux服务器很多小白站长是不敢搞的,不懂代码操作,习惯里windows下界面操作,都很担心使用linux系统不会操作可麻烦,这点我要讲一下,window服务器最不好的一点就是界面化,因为会浪费服务器大量的资源,本来服务器安装linux服务器版,没有界面消耗内存等,可以同时让1000个人访问服务器,可你安装了windows系统只能让600人访问网站,再多的人系统吃不消了。针对于windows这个消耗内存大bug,linux系统为啥领导大型服务器市场份额,就是因为把没必要的资源给了界面,这样本来服务器可以干一件事,现在只能干半件事。所以企业和技术站长都采用linux服务器,尤其是centos 7.6 系统,是消耗资源最好,功能最好的linux服务器,像阿里云、华为云、百度云、腾讯云等默认都是centos系统,这是因为程序员都知道哪个好,肯定主动推送。说完服务器系统,都知道要选linux,但对于远端linux服务器如何管理,如何操作这点,很多人想到了linux面板,对于很对站长想用linux系统是都会了解到linux面板。最早的linux面板是为了让不懂代码的小白站长,能够实现一键搭建web环境,建站而来的,多以市场上诞生了很多国内的单机linux面板。慢慢用户发现,不只是使用linux面板,还需要安全,不然服务器就是一层窗户纸,由于插件工具越来越多,linux面板本来是解决服务器轻量级使用的,现在单机版linux面板必须要在服务器上安装一个网站,再安装各种插件工具,一大堆东西,才能够使用,这就违背了当初linux面板的初衷,是轻量级便捷化。完完全全的软件功能了,现在大环境云端下轻量级设计理念,显然单机版linux面板已经可以淘汰了。现在市场上出现了云端版linux面板,旗鱼云梯这个品牌,能够让服务器和云端平台加密链接,所有功能页面插件等都安装在云端,你的服务器只需要和云端平台链接,就可以直接使用这些工具,让运维网站可以实现批量化,而不是只能管理一台主机。由于旗鱼云梯的集群化批量化功能,让云端linux面板变得可能,让个人站长能够使用这种平台化方式来管理自己的服务器。大大减少了站长对于运维价格高,管理难度大,尤其安全防护方面,云端版通过大数据可以拦截攻击,让服务器一直处于防火墙的保护下,避免服务器遭受黑客攻击等事故而宕机。总结:旗鱼云梯是现在linux系统下云服务器最好的管理平台,可以实现同时管理上百条服务器,批量管理批量操作,让运维服务器变得很简单。

June 11, 2019 · 1 min · jiezi

OpenResty从入门到实战-课程返现福利

系统掌握一款高性能开发利器 《 OpenResty 从入门到实战 》课程大纲可以说,掌握了 OpenResty,你就可以同时拥有脚本语言的开发效率和迭代速度,以及 NGINX C 模块的高并发和高性能优势。《OpenResty从入门到实战》 作者温铭,同时也是 OpenResty 软件基金会主席,《OpenResty 最佳实践》作者。 关注有课学微信公众号,回复暗号 OR 获取购买地址,购买成功后提交购买截图即可获得返现。

May 22, 2019 · 1 min · jiezi

php-redis-lua-实现一个简单的发号器2-实现篇

接着上一篇 php + redis + lua 实现一个简单的发号器(1)-- 原理篇,本篇讲一下发号器的具体实现。 1、基础知识发号器的实现主要用到了下面的一些知识点: 1. php中的位运算的操作和求值 2. 计算机原码、补码、反码的基本概念 3. redis中lua脚本的编写和调试 如果你对这些知识已经熟悉,直接往下看即可, 不了解的话就猛戳。 2、具体实现先上代码吧,然后再慢慢分析 class SignGenerator { CONST BITS_FULL = 64; CONST BITS_PRE = 1;//固定 CONST BITS_TIME = 41;//毫秒时间戳 可以最多支持69年 CONST BITS_SERVER = 5; //服务器最多支持32台 CONST BITS_WORKER = 5; //最多支持32种业务 CONST BITS_SEQUENCE = 12; //一毫秒内支持4096个请求 CONST OFFSET_TIME = "2019-05-05 00:00:00";//时间戳起点时间 /** * 服务器id */ protected $serverId; /** * 业务id */ protected $workerId; /** * 实例 */ protected static $instance; /** * redis 服务 */ protected static $redis; /** * 获取单个实例 */ public static function getInstance($redis) { if(isset(self::$instance)) { return self::$instance; } else { return self::$instance = new self($redis); } } /** * 构造初始化实例 */ protected function __construct($redis) { if($redis instanceof \Redis || $redis instanceof \Predis\Client) { self::$redis = $redis; } else { throw new \Exception("redis service is lost"); } } /** * 获取唯一值 */ public function getNumber() { if(!isset($this->serverId)) { throw new \Exception("serverId is lost"); } if(!isset($this->workerId)) { throw new \Exception("workerId is lost"); } do{ $id = pow(2,self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE; //时间戳 41位 $nowTime = (int)(microtime(true) * 1000); $startTime = (int)(strtotime(self::OFFSET_TIME) * 1000); $diffTime = $nowTime - $startTime; $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME; $id |= $diffTime << $shift; echo "diffTime=",$diffTime,"\t"; //服务器 $shift = $shift - self::BITS_SERVER; $id |= $this->serverId << $shift; echo "serverId=",$this->serverId,"\t"; //业务 $shift = $shift - self::BITS_WORKER; $id |= $this->workerId << $shift; echo "workerId=",$this->workerId,"\t"; //自增值 $sequenceNumber = $this->getSequence($id); echo "sequenceNumber=",$sequenceNumber,"\t"; if($sequenceNumber > pow(2, self::BITS_SEQUENCE)) { usleep(1000); } else { $id |= $sequenceNumber; return $id; } } while(true); } /** * 反解获取业务数据 */ public function reverseNumber($number) { $uuidItem = []; $shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME; $uuidItem['diffTime'] = ($number >> $shift) & (pow(2, self::BITS_TIME) - 1); $shift -= self::BITS_SERVER; $uuidItem['serverId'] = ($number >> $shift) & (pow(2, self::BITS_SERVER) - 1); $shift -= self::BITS_WORKER; $uuidItem['workerId'] = ($number >> $shift) & (pow(2, self::BITS_WORKER) - 1); $shift -= self::BITS_SEQUENCE; $uuidItem['sequenceNumber'] = ($number >> $shift) & (pow(2, self::BITS_SEQUENCE) - 1); $time = (int)($uuidItem['diffTime']/1000) + strtotime(self::OFFSET_TIME); $uuidItem['generateTime'] = date("Y-m-d H:i:s", $time); return $uuidItem; } /** * 获取自增序列 */ protected function getSequence($id) { $lua = <<<LUA local sequenceKey = KEYS[1] local sequenceNumber = redis.call("incr", sequenceKey); redis.call("pexpire", sequenceKey, 1); return sequenceNumberLUA; $sequence = self::$redis->eval($lua, [$id], 1); $luaError = self::$redis->getLastError(); if(isset($luaError)) { throw new \ErrorException($luaError); } else { return $sequence; } } /** * @return mixed */ public function getServerId() { return $this->serverId; } /** * @param mixed $serverId */ public function setServerId($serverId) { $this->serverId = $serverId; return $this; } /** * @return mixed */ public function getWorkerId() { return $this->workerId; } /** * @param mixed $workerId */ public function setWorkerId($workerId) { $this->workerId = $workerId; return $this; } }3、运行一把获取uuid ...

May 9, 2019 · 4 min · jiezi

php-redis-lua-实现一个简单的发号器1-原理篇

1、为什么要实现发号器很多地方我们都需要一个全局唯一的编号,也就是uuid。举一个常见的场景,电商系统产生订单的时候,需要有一个对应的订单编号。在composer上我们也可以看到有很多可以产生uuid的优秀组件。那么,为什么我们还要自己实现发号器,来产生uuid呢?想了一下,主要有两个原因吧: 1、我希望uuid是可反解的,通过反解uuid可以得出和我业务相关的数据。而我看到的composer关于uuid的相关组件,生成的都是一串指定格式的字符串,我很难将它同具体的业务关联起来。 2、我希望通过uuid是可以随着并放量进行调整的。比如说原有支持1秒钟可以产生1000个uuid,但随着业务规模增长,我希望变成可以支持1秒钟产生一万个。而且,最好改下配置就可以了。 出于以上两个原因,我们需要自己的发号器来产生uuid。那么,下一个问题是,我们应该如何实现发号器,实现发号器的原理又是什么呢? 2、snowFlake算法关于发号器的实现原理,可能大家都听过鼎鼎大名的snowflake算法 -- 雪花算法,Twitter的分布式自增Id算法。国内的新浪微博也有自己实现的发号器算法,具体实现细节虽有不同,但是原理相通,明白其中一个即可。这里我们主要介绍snowflake。 关于snowflaw的介绍,已经有很多文章进行介绍,而且写的也很不错,我没有必要在重写一遍,拿来粘贴即可,出于对作者的尊重,我会将原文链接添加到参考链接中。 推特的分布式自增ID算法,使用long (8 × 8 = 64 byte)来保存uuid。其中1bit留给固定符号位0,41bit留给毫秒时间戳,10bit给MachineID,也就是机器要预先配置,剩下12位留Sequence(可支持1毫秒内4096个请求)。 也许有的人会问如果超过了1毫秒4096个请求怎么办?一般的做法是,让它等上1毫秒,促使41bit的时间戳变化。 这里我们将MachineId进行了拆分,5byte留给机器(最多可以支持32机器),5byte留给了业务号(最多可支持32种业务) 这里的时间戳保存的是当前时间与固定过去时间得一个差值,不是当前时间。这样的好处是能使用更长时间,而且不受年份限制,只取决于从什么时候开始用的,2^41 / 1000360024*365=69年。 如果保存的是当前时间戳,最多只能使用到2039年。2^41=2199023255552=2039/9/7 23:47:35理论上单机速度:2^12*1000 = 4 096 000/s 3、如何保证在单位时间内持续递增通过对snowflake的初步了解,发现,其实发号器也是建立在时间戳基础之上的,因为时间是天然的唯一元素。但是,如何在单位时间内,比如说一秒钟或者一毫秒之内,保证Sequence持续递增才是发号器实现的关键。 这里我们实现的方式比较简单,直接使用redis的incr进行计数,对应的key就是毫秒时间戳。出于redis内存回收的考虑,我们需要将每一个key设置过期时间。如果key是秒级别的时间戳,那么过期时间就是1秒;如果key毫秒级别的时间戳,那么过期时间就是1毫秒。 与此同时,为了保证执行incr,expire(pexpire)具有原子性,我们使用lua来进行实现。 好了,实现的思路大致如此。由于能力和水平有限,难免会有纰漏,希望及时指出。 4、参考文章分布式ID生成器PHP+Swoole实现(上) - 实现原理

May 7, 2019 · 1 min · jiezi

OpenResty下使用Apache Ant Path匹配库

OpenResty下使用Apache Ant Path匹配库一、简介 OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,而lua相对于编译型语言性能比较差,所以我们使用编写sharedobject库的方式集成到OpenResty项目中去。luajit使用ffi调用libcgoantpath.so来实现pattern匹配。 基于以上思路我们实现了一个符合Apache Ant Path标准的动态共享库,Git地址:go-antpath v1.1,为了方大家使用我们还封装了lua版本的lua-antpath v1.0.1,欢迎大家多多指导,共同进步。二、参考http://ant.apache.org/manual/api/org/apache/tools/ant/https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/PathMatcher.htmlgo-antpathlua-antpathhttps://github.com/golang/go/wiki/cgohttps://golang.org/cmd/cgo/https://groups.google.com/forum/#!topic/golang-nuts/Nb-nfVdAyF0三、编译及运行环境3.1 编译环境GNU Make 4.1 golang 1.9.2+3.2 运行环境luajit 2.1 antpath.go (执行make的时候自动下载)lua2go v1.0 (执行make的时候自动下载) cjson (OpenResty自带优良库)四、使用

April 10, 2019 · 1 min · jiezi

基于Redis和Lua的分布式限流

Java单机限流可以使用AtomicInteger,RateLimiter或Semaphore来实现,但是上述方案都不支持集群限流。集群限流的应用场景有两个,一个是网关,常用的方案有Nginx限流和Spring Cloud Gateway,另一个场景是与外部或者下游服务接口的交互,因为接口限制必须进行限流。 本文的主要内容为:Redis和Lua的使用场景和注意事项,比如说KEY映射的问题。Spring Cloud Gateway中限流的实现。集群限流的难点 在上篇Guava RateLimiter的分析文章中,我们学习了令牌桶限流算法的原理,下面我们就探讨一下,如果将RateLimiter扩展,让它支持集群限流,会遇到哪些问题。 RateLimiter会维护两个关键的参数nextFreeTicketMicros和storedPermits,它们分别是下一次填充时间和当前存储的令牌数。当RateLimiter的acquire函数被调用时,也就是有线程希望获取令牌时,RateLimiter会对比当前时间和nextFreeTicketMicros,根据二者差距,刷新storedPermits,然后再判断更新后的storedPermits是否足够,足够则直接返回,否则需要等待直到令牌足够(Guava RateLimiter的实现比较特殊,并不是当前获取令牌的线程等待,而是下一个获取令牌的线程等待)。 由于要支持集群限流,所以nextFreeTicketMicros和storedPermits这两个参数不能只存在JVM的内存中,必须有一个集中式存储的地方。而且,由于算法要先获取两个参数的值,计算后在更新两个数值,这里涉及到竞态限制,必须要处理并发问题。 集群限流由于会面对相比单机更大的流量冲击,所以一般不会进行线程等待,而是直接进行丢弃,因为如果让拿不到令牌的线程进行睡眠,会导致大量的线程堆积,线程持有的资源也不会释放,反而容易拖垮服务器。Redis和Lua 分布式限流本质上是一个集群并发问题,Redis单进程单线程的特性,天然可以解决分布式集群的并发问题。所以很多分布式限流都基于Redis,比如说Spring Cloud的网关组件Gateway。 Redis执行Lua脚本会以原子性方式进行,单线程的方式执行脚本,在执行脚本时不会再执行其他脚本或命令。并且,Redis只要开始执行Lua脚本,就会一直执行完该脚本再进行其他操作,所以Lua脚本中不能进行耗时操作。使用Lua脚本,还可以减少与Redis的交互,减少网络请求的次数。 Redis中使用Lua脚本的场景有很多,比如说分布式锁,限流,秒杀等,总结起来,下面两种情况下可以使用Lua脚本:使用 Lua 脚本实现原子性操作的CAS,避免不同客户端先读Redis数据,经过计算后再写数据造成的并发问题。前后多次请求的结果有依赖时,使用 Lua 脚本将多个请求整合为一个请求。 但是使用Lua脚本也有一些注意事项:要保证安全性,在 Lua 脚本中不要定义自己的全局变量,以免污染 Redis内嵌的Lua环境。因为Lua脚本中你会使用一些预制的全局变量,比如说redis.call()要注意 Lua 脚本的时间复杂度,Redis 的单线程同样会阻塞在 Lua 脚本的执行中。使用 Lua 脚本实现原子操作时,要注意如果 Lua 脚本报错,之前的命令无法回滚,这和Redis所谓的事务机制是相同的。一次发出多个 Redis 请求,但请求前后无依赖时,使用 pipeline,比 Lua 脚本方便。Redis要求单个Lua脚本操作的key必须在同一个Redis节点上。解决方案可以看下文对Gateway原理的解析。性能测试 Redis虽然以单进程单线程模型进行操作,但是它的性能却十分优秀。总结来说,主要是因为:绝大部分请求是纯粹的内存操作采用单线程,避免了不必要的上下文切换和竞争条件内部实现采用非阻塞IO和epoll,基于epoll自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。 所以,在集群限流时使用Redis和Lua的组合并不会引入过多的性能损耗。我们下面就简单的测试一下,顺便熟悉一下涉及的Redis命令。# test.lua脚本的内容local test = redis.call(“get”, “test”)local time = redis.call(“get”, “time”)redis.call(“setex”, “test”, 10, “xx”)redis.call(“setex”, “time”, 10, “xx”)return {test, time}# 将脚本导入redis,之后调用不需再传递脚本内容redis-cli -a 082203 script load “$(cat test.lua)““b978c97518ae7c1e30f246d920f8e3c321c76907”# 使用redis-benchmark和evalsha来执行lua脚本redis-benchmark -a 082203 -n 1000000 evalsha b978c97518ae7c1e30f246d920f8e3c321c76907 0 ======1000000 requests completed in 20.00 seconds50 parallel clients3 bytes payloadkeep alive: 193.54% <= 1 milliseconds99.90% <= 2 milliseconds99.97% <= 3 milliseconds99.98% <= 4 milliseconds99.99% <= 5 milliseconds100.00% <= 6 milliseconds100.00% <= 7 milliseconds100.00% <= 7 milliseconds49997.50 requests per second 通过上述简单的测试,我们可以发现本机情况下,使用Redis执行Lua脚本的性能极其优秀,一百万次执行,99.99%在5毫秒以下。 本来想找一下官方的性能数据,但是针对Redis + Lua的性能数据较少,只找到了几篇个人博客,感兴趣的同学可以去探索。这篇文章有Lua和zadd的性能比较(具体数据请看原文,链接缺失的话,请看文末)。以上lua脚本的性能大概是zadd的70%-80%,但是在可接受的范围内,在生产环境可以使用。负载大概是zadd的1.5-2倍,网络流量相差不大,IO是zadd的3倍,可能是开启了AOF,执行了三次操作。Spring Cloud Gateway的限流实现 Gateway是微服务架构Spring Cloud的网关组件,它基于Redis和Lua实现了令牌桶算法的限流功能,下面我们就来看一下它的原理和细节吧。 Gateway基于Filter模式,提供了限流过滤器RequestRateLimiterGatewayFilterFactory。只需在其配置文件中进行配置,就可以使用。具体的配置感兴趣的同学自行学习,我们直接来看它的实现。 RequestRateLimiterGatewayFilterFactory依赖RedisRateLimiter的isAllowed函数来判断一个请求是否要被限流抛弃。public Mono<Response> isAllowed(String routeId, String id) { //routeId是ip地址,id是使用KeyResolver获取的限流维度id,比如说基于uri,IP或者用户等等。 Config routeConfig = loadConfiguration(routeId); // 每秒能够通过的请求数 int replenishRate = routeConfig.getReplenishRate(); // 最大流量 int burstCapacity = routeConfig.getBurstCapacity(); try { // 组装Lua脚本的KEY List<String> keys = getKeys(id); // 组装Lua脚本需要的参数,1是指一次获取一个令牌 List<String> scriptArgs = Arrays.asList(replenishRate + “”, burstCapacity + “”, Instant.now().getEpochSecond() + “”, “1”); // 调用Redis,tokens_left = redis.eval(SCRIPT, keys, args) Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs); ….. // 省略 }static List<String> getKeys(String id) { String prefix = “request_rate_limiter.{” + id; String tokenKey = prefix + “}.tokens”; String timestampKey = prefix + “}.timestamp”; return Arrays.asList(tokenKey, timestampKey);} 需要注意的是getKeys函数的prefix包含了”{id}",这是为了解决Redis集群键值映射问题。Redis的KeySlot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。Redis要求单个Lua脚本操作的key必须在同一个节点上,但是Cluster会将数据自动分布到不同的节点,使用这种方法就解决了上述的问题。 然后我们来看一下Lua脚本的实现,该脚本就在Gateway项目的resource文件夹下。它就是如同Guava的RateLimiter一样,实现了令牌桶算法,只不过不在需要进行线程休眠,而是直接返回是否能够获取。local tokens_key = KEYS[1] – request_rate_limiter.${id}.tokens 令牌桶剩余令牌数的KEY值local timestamp_key = KEYS[2] – 令牌桶最后填充令牌时间的KEY值local rate = tonumber(ARGV[1]) – replenishRate 令令牌桶填充平均速率local capacity = tonumber(ARGV[2]) – burstCapacity 令牌桶上限local now = tonumber(ARGV[3]) – 得到从 1970-01-01 00:00:00 开始的秒数local requested = tonumber(ARGV[4]) – 消耗令牌数量,默认 1 local fill_time = capacity/rate – 计算令牌桶填充满令牌需要多久时间local ttl = math.floor(fill_time*2) – 2 保证时间充足local last_tokens = tonumber(redis.call(“get”, tokens_key)) – 获得令牌桶剩余令牌数if last_tokens == nil then – 第一次时,没有数值,所以桶时满的 last_tokens = capacityendlocal last_refreshed = tonumber(redis.call(“get”, timestamp_key)) – 令牌桶最后填充令牌时间if last_refreshed == nil then last_refreshed = 0endlocal delta = math.max(0, now-last_refreshed) – 获取距离上一次刷新的时间间隔local filled_tokens = math.min(capacity, last_tokens+(deltarate)) – 填充令牌,计算新的令牌桶剩余令牌数 填充不超过令牌桶令牌上限。local allowed = filled_tokens >= requested local new_tokens = filled_tokenslocal allowed_num = 0if allowed then– 若成功,令牌桶剩余令牌数(new_tokens) 减消耗令牌数( requested ),并设置获取成功( allowed_num = 1 ) 。 new_tokens = filled_tokens - requested allowed_num = 1end – 设置令牌桶剩余令牌数( new_tokens ) ,令牌桶最后填充令牌时间(now) ttl是超时时间?redis.call(“setex”, tokens_key, ttl, new_tokens)redis.call(“setex”, timestamp_key, ttl, now)– 返回数组结果return { allowed_num, new_tokens }后记 Redis的主从异步复制机制可能丢失数据,出现限流流量计算不准确的情况,当然限流毕竟不同于分布式锁这种场景,对于结果的精确性要求不是很高,即使多流入一些流量,也不会影响太大。 正如Martin在他质疑Redis分布式锁RedLock文章中说的,Redis的数据丢弃了也无所谓时再使用Redis存储数据。I think it’s a good fit in situations where you want to share some transient, approximate, fast-changing data between servers, and where it’s not a big deal if you occasionally lose that data for whatever reason 接下来我们回来学习阿里开源的分布式限流组件sentinel,希望大家持续关注。 个人博客: Remcarpediem参考https://www.cnblogs.com/itren…压测的文章:https://www.fuwuqizhijia.com/…https://blog.csdn.net/forezp/...https://blog.csdn.net/xixingz...Matin RedLock http://martin.kleppmann.com/2… ...

April 8, 2019 · 2 min · jiezi

php 使用 lua+redis 限流,计数器模式,令牌桶模式

lua 优点减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.计数器模式:利用lua脚本一次性完成处理达到原子性,通过INCR自增计数,判断是否达到限定值,达到限定值则返回限流,添加key过期时间应该范围过度$lua = ’ local i = redis.call(“INCR”, KEYS[1]) if i > 10 then return “wait” else if i == 1 then redis.call(“expire”, KEYS[1], KEYS[2]) end return redis.call(“get”, KEYS[3]) end ‘;laravel 请求代码: Redis::eval($lua, 3, sprintf(RedisKey::API_LIMIT, $key, $callService[‘service’]), 60, $cache_key);令牌桶模式每次请求在桶内拿取一个令牌,有令牌则通过,否则返回,并且按照算法一定的慢慢把令牌放入桶内$lua = ’ local data = redis.call(“get”, KEYS[2]) if data then local dataJson = cjson.decode(data) local newNum = math.min(KEYS[3], math.floor(((dataJson[“limitVal”] - 1) + (KEYS[3]/KEYS[5]) * (KEYS[4] - dataJson[“limitTime”])))) if newNum > 0 then local paramsJson = cjson.encode({limitVal=newNum,limitTime=KEYS[4]}) redis.call(“set”, KEYS[2], paramsJson) return redis.call(“get”, KEYS[1]) end return “wait” end local paramsJson = cjson.encode({limitVal=KEYS[3],limitTime=KEYS[4]}) redis.call(“set”, KEYS[2], paramsJson) return redis.call(“get”, KEYS[1]) ‘; // 1. lua脚本, 2 KEYS数量, 3 查找数据key, 4 限制key, 5 桶内数量, 6 时间戳, 7 过期时间 Redis::eval(1,2,3,4,5,6,7参数); ...

April 4, 2019 · 1 min · jiezi

Lua在Nginx的应用

首发于 樊浩柏科学院当 Nginx 标准模块和配置不能灵活地适应系统要求时,就可以考虑使用 Lua 扩展和定制 Nginx 服务。OpenResty 集成了大量精良的 Lua 库、第三方模块,可以方便地搭建能够处理超高并发、扩展性极高的 Web 服务,所以这里选择 OpenResty 提供的 lua-nginx-module 方案。安装Lua环境lua-nginx-module 依赖于 LuaJIT 和 ngx_devel_kit。LuaJIT 需要安装,ngx_devel_kit 只需下载源码包,在 Nginx 编译时指定 ngx_devel_kit 目录。系统依赖库首先确保系统已安装如下依赖库。$ yum install readline-devel pcre-devel openssl-devel gcc安装LuaJIT首先,安装 LuaJIT 环境,如下所示:$ wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz$ tar zxvf LuaJIT-2.0.5.tar.gz$ cd LuaJIT-2.0.5$ make install# 安装成功==== Successfully installed LuaJIT 2.0.5 to /usr/local ====设置 LuaJIT 有关的环境变量。$ export LUAJIT_LIB=/usr/local/lib$ export LUAJIT_INC=/usr/local/include/luajit-2.0$ echo “/usr/local/lib” > /etc/ld.so.conf.d/usr_local_lib.conf$ ldconfig下载相关模块下载 ngx_devel_kit 源码包,如下:$ wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz$ tar zxvf v0.3.0.tar.gz# 解压缩后目录名ngx_devel_kit-0.3.0接下来,下载 Lua 模块 lua-nginx-module 源码包,为 Nginx 编译作准备。$ wget https://github.com/openresty/lua-nginx-module/archive/v0.10.10.tar.gz$ tar zxvf v0.10.10.tar.gz# 解压缩后目录名lua-nginx-module-0.10.10加载Lua模块Nginx 1.9 版本后可以动态加载模块,但这里由于版本太低只能重新编译安装 Nginx。下载 Nginx 源码包并解压:$ wget http://nginx.org/download/nginx-1.13.5.tar.gz$ tar zxvf nginx-1.13.5.tar.gz编译并重新安装 Nginx:$ cd nginx-1.13.5# 增加–add-module=/usr/src/lua-nginx-module-0.10.10 –add-module=/usr/src/ngx_devel_kit-0.3.0$ ./configure –prefix=/usr/local/nginx –with-http_ssl_module –with-http_v2_module –with-http_stub_status_module –with-pcre –add-module=/usr/src/lua-nginx-module-0.10.10 –add-module=/usr/src/ngx_devel_kit-0.3.0$ make$ make install# 查看是否安装成功$ nginx -v配置Nginx环境现在只需配置 Nginx,即可嵌入 Lua 脚本。首先,在 http 部分配置 Lua 模块和第三方库路径:# 第三方库(cjson)地址luajit-2.0/liblua_package_path ‘/home/www/lua/?.lua;;’;lua_package_cpath ‘/usr/local/include/luajit-2.0/lib/?.so;;’;接着,配置一个 Lua 脚本服务:# hello world测试server { location /lua_content { # 定义MIME类型 default_type ’text/plain’; content_by_lua_block { ngx.say(‘Hello,world!’) } }}测试安装和配置是否正常:$ service nginx test$ service nginx reload# 访问地址/lua_content输出Hello,world!Lua调用Nginxlua-nginx-module 模块中已经为 Lua 提供了丰富的 Nginx 调用 API,每个 API 都有各自的作用环境,详细描述见 Nginx API for Lua。这里只列举基本 API 的使用 。先配一个 Lua 脚本服务,配置文件如下:location ~ /lua_api { # 示例用的Nginx变量 set $name $host; default_type “text/html”; # 通过Lua文件进行内容处理 content_by_lua_file /home/www/nginx-api.lua; }请求部分ngx.var可以通过ngx.var.var_name形式获取或设置 Nginx 变量值,例如 request_uri、host、request 等。– ngx.say打印内容ngx.say(ngx.var.request_uri)ngx.var.name = ‘www.fanhaobai.com’ngx.req.get_headers()该方法会以表的形式返回当前请求的头信息。查看请求的头信息:ngx.say(‘Host : ‘, ngx.req.get_headers().host, ‘<br>’)for k,v in pairs(ngx.req.get_headers()) do if type(v) == “table” then ngx.say(k, “ : ”, table.concat(v, “,”), ‘<br>’) else ngx.say(k," : ", v, ‘<br>’) endend当然,通过 ngx.req.set_header() 也可以设置头信息。ngx.req.set_header(“Content-Type”, “text/html”)ngx.req.get_uri_args()该方法以表形式返回当前请求的所有 GET 参数。查看请求 query 为?name=fhb的 GET 参数:ngx.say(’name : ‘, ngx.req.get_uri_args().name, ‘<br>’)for k,v in pairs(ngx.req.get_uri_args()) do if type(v) == “table” then ngx.say(k, “ : ”, table.concat(v, “,”), ‘<br>’) else ngx.say(k," : ", v, ‘<br>’) endend同样,可以通过 ngx.req.set_uri_args() 设置请求的所有 GET 参数。ngx.req.set_uri_args({name=‘fhb’}) –{name=‘fhb’}可以为query形式name=fhbget_post_args()该方法以表形式返回当前请求的所有 POST 参数,POST 数据必须是 application/x-www-form-urlencoded 类型。查看请求curl –data ’name=fhb’ localhost/lua_api的 POST 参数:–必须先读取body体ngx.req.read_body()ngx.say(’name : ‘, ngx.req.get_post_args().name, ‘<br>’)for k,v in pairs(ngx.req.get_post_args()) do if type(v) == “table” then ngx.say(k, “ : ”, table.concat(v, “,”), ‘<br>’) else ngx.say(k," : ", v, ‘<br>’) endend通过 ngx.req.get_body_data() 方法可以获取未解析的请求 body 体内容字符串。ngx.req.get_method()获取请求的大写字母形式的请求方式,通过 ngx.req.set_method() 可以设置请求方式。例如:ngx.say(ngx.req.get_method())响应部分ngx.header通过ngx.header.header_name的形式获取或设置响应头信息。如下:ngx.say(ngx.header.content_type)ngx.header.content_type = ’text/plain’ngx.print()ngx.print() 方法会填充指定内容到响应 body 中。如下所示:ngx.print(ngx.header.content_type)ngx.say()如上述使用,ngx.say() 方法同 ngx.print() 方法,只是会在后追加一个换行符。ngx.exit()以某个状态码返回响应内容,状态码常量对应关系见 HTTP status constants 部分,也支持数字形式的状态码。ngx.exit(403)ngx.redirect()重定向当前请求到新的 url,响应状态码可选列表为 301、302(默认)、303、307。ngx.redirect(‘http://www.fanhaobai.com’)其他ngx.re.match该方法提供了正则表达式匹配方法。请求?name=fhb&age=24匹配 GET 参数中的数字:local m, err = ngx.re.match(ngx.req.set_uri_args, “[0-9]+")if m then ngx.say(m[0])else ngx.say(“match not found”)endngx.log()通过该方法可以将内容写入 Nginx 日志文件,日志文件级别需同 log 级别一致。ngx.md5() | ngx.encode_base64() | ngx.decode_base64()它们都是字符串编码方式。ngx.md5() 可以对字符串进行 md5 加密处理,而 ngx.encode_base64() 是对字符串 base64 编码, ngx.decode_base64() 为 base64 解码。Nginx中嵌入Lua上面讲述了怎么在 Lua 中调用 Nginx 的 API 来扩展或定制 Nginx 的功能,那么编写好的 Lua 脚本怎么在 Nginx 中得到执行呢?其实,Nginx 是通过模块指令形式在其 11 个处理阶段做插入式处理,指令覆盖 http、server、server if、location、location if 这几个范围。模块指令列表这里只列举基本的 Lua 模块指令,更多信息参考 Directives 部分。指令所在阶段使用范围说明init_by_luainit_by_lua_file加载配置文件http可以用于初始化全局配置set_by_luaset_by_lua_filerewriteserverlocationlocation if复杂逻辑的变量赋值,注意是阻塞的rewrite_by_luarewrite_by_lua_filerewritehttpserverlocationlocation if实现复杂逻辑的转发或重定向content_by_luacontent_by_lua_filecontentlocationlocation if处理请求并输出响应header_filter_by_luaheader_filter_by_lua_file响应头信息过滤httpserverlocationlocation if设置响应头信息body_filter_by_luabody_filter_by_lua_file输出过滤httpserverlocationlocation if对输出进行过滤或修改使用指令注意到,每个指令都会有*_lua和*_lua_file两个指令,_lua指令后为 Lua 代码块,而_lua_file指令后为 Lua 脚本文件路径。下面将只对*_lua指令进行说明。init_by_lua该指令会在 Nginx 的 Master 进程加载配置时执行,所以可以完成 Lua 模块初始化工作,Worker 进程同样会继承这些。nginx.conf配置文件中的 http 部分添加如下代码:– 所有worker共享的全局变量lua_shared_dict shared_data 1m; init_by_lua_file /usr/example/lua/init.lua;init.lua初始化脚本为:local cjson = require ‘cjson’local redis = require ‘resty.redis’local shared_data = ngx.shared.shared_dataset_by_lua我们直接使用 set 指令很难实现很复杂的变量赋值逻辑,而 set_by_lua 模块指令就可以解决这个问题。nginx.conf配置文件 location 部分内容为:location /lua { set_by_lua_file $num /home/www/set.lua; default_type ’text/html’; echo $num;}set.lua脚本内容为:local uri_args = ngx.req.get_uri_args()local i = uri_args.a or 0local j = uri_args.b or 0return i + j上述赋值逻辑,请求 query 为?a=10&b=2时响应内容为 12。rewrite_by_lua可以实现内部 URL 重写或者外部重定向。nginx.conf配置如下:location /lua { default_type “text/html”; rewrite_by_lua_file /home/www/rewrite.lua;}rewrite.lua脚本内容:if ngx.req.get_uri_args()[“type”] == “app” then ngx.req.set_uri("/m_h5”, false);endaccess_by_lua用于访问权限控制。例如,只允许带有身份标识用户访问,nginx.conf配置为:location /lua { default_type “text/html”; access_by_lua_file /home/www/access.lua;} access.lua脚本内容为:if ngx.req.get_uri_args()[“token”] == “fanhb” then return ngx.exit(403)endcontent_by_lua该指令在 Lua 调用 Nginx 部分已经使用过了,用于输出响应内容。案例访问权限控制使用 Lua 模块对本站的 ES 服务做受信操作控制,即非受信 IP 只能查询操作。nginx.conf配置如下:location / { set $allowed ‘115.171.226.212’; access_by_lua_block { if ngx.re.match(ngx.req.get_method(), “PUT|POST|DELETE”) and not ngx.re.match(ngx.var.request_uri, “_search”) then start, _ = string.find(ngx.var.allowed, ngx.var.remote_addr) if not start then ngx.exit(403) end end } proxy_pass http://127.0.0.1:9200$request_uri;}访问频率控制在 Nginx 配置文件的 location 部分配置 Lua 脚本基本参数,并配置 Lua 模块指令:default_type “text/html”;set rate_per 300access_by_lua_file /home/www/access.lua;Lua 脚本实现频率控制逻辑,使用 Redis 对单位时间内的访问次数做缓存,key 为访问 uri 拼接 token 后的 md5 值。具体内容如下:local redis = require “resty.redis"local red = redis:new()local limit = tonumber(ngx.var.rate_per) or 200local expire_time = tonumber(ngx.var.rate_expire) or 1000local key = “rate.limit:string:“red:set_timeout(500)local ok, err = red:connect(“www.fanhaobai.com”, 6379)if not ok then ngx.log(ngx.ERR, “failed to connect redis: " .. err) returnendkey = key .. ngx.md5(ngx.var.request_uri .. (ngx.req.get_uri_args()[’token’] or ngx.req.get_post_args()[’token’]))local times, err = red:incr(key)if not times then ngx.log(ngx.ERR, “failed to exec incr: " .. err) returnelseif times == 1 then ok, err = red:expire(key, expire_time) if not ok then ngx.log(ngx.ERR, “failed to exec expire: " .. err) return endendif times > limit then return ngx.exit(403)endreturn相关文章 »进入Lua的世界(2017-09-03)Lua在Redis的应用(2017-09-04) ...

March 25, 2019 · 3 min · jiezi

Lua在Redis的应用

首发于 樊浩柏科学院Redis 从 2.6 版本起,也已开始支持 Lua 脚本,我们可以更加得心应手地使用或扩展 Redis,特别是在高并发场景下 Lua 脚本提供了更高效、可靠的解决方案。为什么要使用Lua我们先看一个抢购场景下 商品库存 的问题,用 PHP 可简单实现为:$key = ’number:string’;$redis = new Redis();$number = $redis->get($key);if ($number <= 0) { return 0;}$redis->decr($key);return $number–;这段代码其实存在问题,高并发时会出现库存超卖的情况,因为上述操作在 Redis 中不是原子操作,会导致库存逻辑的判断失效。尽管可以通过优化代码来解决问题,比如使用 Decr 原子操作命令、或者使用 锁 的方式,但这里使用 Lua 脚本来解决。local key = ’number:string’local number = tonumber(redis.call(“GET”, key))if number <= 0 then return 0endredis.call(“DECR”, key)return number–这段脚本代码虽然是 Lua 语言编写( 进入Lua的世界),但是其实就是 PHP 版本的翻译版。那为什么这样,Lua 脚本就能解决库存问题了呢?Redis 中嵌入 Lua 脚本,所具有的几个特性为:原子操作:Redis 将整个 Lua 脚本作为一个原子执行,无需考虑并发,无需使用事务来保证数据一致性;高性能:嵌入 Lua 脚本后,可以减少多个命令执行的网络开销,进而间接提高 Redis 性能;可复用:Lua 脚本会保存于 Redis 中,客户端都可以使用这些脚本;在Redis中嵌入Lua使用Lua解析器Redis 提供了 EVAL(直接执行脚本) 和 EVALSHA(执行 SHA1 值的脚本) 这两个命令,可以使用内置的 Lua 解析器执行 Lua 脚本。语法格式为:EVAL script numkeys key [key …] arg [arg …]EVALSHA sha1 numkeys key [key …] arg [arg …]参数说明:script / sha1:EVAL 命令的第一个参数为需要执行的 Lua 脚本字符,EVALSHA 命令的一个参数为 Lua 脚本的 SHA1 值numkeys:表示 key 的个数key [key …]:从第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局数组 KYES[i] 访问arg [arg …]:附加参数,在 Lua 中通过全局数组 ARGV[i] 访问EVAL 命令的使用示例:> EVAL “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 2 key1 key2 first second1) “key1"2) “key2"3) “first"4) “second"每次使用 EVAL 命令都会传递需执行的 Lua 脚本内容,这样增加了宽带的浪费。Redis 内部会永久保存被运行在脚本缓存中,所以使用 EVALSHA(建议使用) 命令就可以根据脚本 SHA1 值执行对应的 Lua 脚本。> SCRIPT LOAD “return ‘hello’““1b936e3fe509bcbc9cd0664897bbe8fd0cac101b”> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0"hello"Redis 中执行 Lua 脚本都是以原子方式执行,所以是原子操作。另外,redis-cli 命令行客户端支持直接使用–eval lua_file参数执行 Lua 脚本。Redis 中有关脚本的命令除了 EVAL 和 EVALSHA 外,其他常用命令 如下:命令描述SCRIPT EXISTS script [script …]查看脚本是是否保存在缓存中SCRIPT FLUSH从缓存中移除所有脚本SCRIPT KILL杀死当前运行的脚本SCRIPT LOAD script将脚本添加到缓存中,不立即执行返回脚本SHA1值数据类型的转换由于 Redis 和 Lua 都有各自定义的数据类型,所以在使用执行完 Lua 脚本后,会存在一个数据类型转换的过程。Lua 到 Redis 类型转换与 Redis 到 Lua 类型转换相同部分关系:Lua 类型Redis 返回类型说明numberinteger浮点数会转换为整数3.333–>3stringbulk table(array)multi bulk boolean falsenil > EVAL “return 3.333” 0(integer) 3> EVAL “return ‘fhb’” 0"fhb”> EVAL “return {‘fhb’, ’lw’, ’lbf’}” 01) “fhb"2) “lw"3) “lbf”> EVAL “return false” 0(nil)需要注意的是,从 Lua 转化为 Redis 类型比 Redis 转化为 Lua 类型多了一条 额外 规则:Lua 类型Redis 返回类型说明boolean trueinteger返回整型 1> EVAL “return true” 0(integer) 1总而言之,类型转换的原则 是将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。全局变量保护为了防止不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。– 定义全局函数function f(n) return n * 2endreturn f(4);执行redis-cli –eval function.lua命令,会抛出尝试定义全局变量的错误:(error) ERR Error running script (call to f_0a602c93c4a2064f8dc648c402aa27d68b69514f): @enable_strict_lua:8: user_script:1: Script attempted to create global variable ‘f’Lua脚本调用Redis命令Redis 创建了用于与 Lua 环境协作的组件—— 伪客户端,它负责执行 Lua 脚本中的 Redis 命令。调用Redis命令在 Redis 内置的 Lua 解析器中,调用 redis.call() 和 redis.pcall() 函数执行 Redis 的命令。它们除了处理错误的行为不一样外,其他行为都保持一致。调用 格式:redis.call(command, [key …], arg [arg …] )redis.pcall(command, [key …], arg [arg …] )> EVAL “return redis.call(‘SET’, ’name’, ‘fhb’)” 0> EVAL “return redis.pcall(‘GET’, ’name’)” 0"fhb"Redis日志在 Lua 脚本中,可以通过调用 redis.log() 函数来写 Redis 日志。格式为:redis.log(loglevel, message)loglevel 参数可以是 redis.LOG_DEBUG、redis.LOG_VERBOSE、redis.LOG_NOTICE、redis.LOG_WARNING 的任意值。查看redis.conf日志配置信息:# logleval必须一致才会记录loglevel noticelogfile “/home/logs/redis.log"Lua 写 Redis 日志示例:> EVAL “redis.log(redis.LOG_NOTICE, ‘I am fhb’)” 0113:M 04 Sep 13:12:36.229 * I am fhb案例API 访问速率控制通过 Lua 实现一个针对用户的 API 访问速率控制,Lua 代码如下:local key = “rate.limit:string:” .. KEYS[1]local limit = tonumber(ARGV[1])local expire_time = tonumber(ARGV[2])local times = redis.call(“INCR”, key)if times == 1 then redis.call(“EXPIRE”, key, expire_time)endif times > limit then return 0endreturn 1KEYS[1] 可以用 API 的 URI + 用户 uid 组成,ARGV[1] 为单位时间限制访问的次数,ARGV[2] 为限制的单位时间。批量HGETTALL这个例子演示通过 Lua 实现批量 HGETALL,当然也可以使用 管道 实现。– KEYS为uid数组local users = {}for i,uid in ipairs(KEYS) do local user = redis.call(‘hgetall’, uid) if user ~= nil then table.insert(users, i, user) endendreturn users注意事项虽然使用 Lua 脚本给我们带来了许多便利,但是需要注意几个使用事项:Lua 脚本在执行时是阻塞的,不应该在 Lua 脚本中有耗时的处理逻辑;在集群模式时,Lua 脚本必须使用参数 key 传递需操作的 Redis 的 key,且要求所操作的 key 都在同一个 slot 节点上,可以使用以{}标记的 hash tag 方式解决。相关文章 »进入Lua的世界(2017-09-03)Lua在Nginx的应用(2017-09-09) ...

March 25, 2019 · 3 min · jiezi

进入Lua的世界

首发于 樊浩柏科学院Lua 是一个扩展式程序设计语言,作为一个强大、轻量的脚本语言,可以嵌入任何需要的程序中使用。Lua 被设计成一种动态类型语言,且它的语法相对较简单,这里只介绍其基本语法和使用方法,更多信息见 Lua 5.3 参考手册。数据类型Lua 作为通用型脚本语言,有 8 种基本数据类型:类型说明示例nil只有一种值 nil标识和别的任何值的差异nilboolean两种值 false 和 truefalsenumber实数(双精度浮点数)520string字符串,不区分单双引号“fhb”‘fhb’function函数function haha() return 1enduserdata将任意 C 数据保存在 Lua 变量 thread区别独立的执行线程用来实现协程 table表,实现了一个关联数组唯一一种数据结构{1, 2, 3}使用库函数 type() 可以返回一个变量或标量的类型。有关数据类型需要说明的是:nil 和 false 都能导致条件为假,而另外所有的值都被当作真在 number 和 string 类型参与比较或者运算时,会存在隐式类型转化,当然也可以显示转化(tonumber())由于 table、 function、thread、userdata 的值是所谓的对象,变量本身只是一个对对象的引用,所以赋值、参数传递、函数返回,都是对这些对象的引用传递变量Lua 中有三类变量:全局变量、局部变量、还有 table 的域。任何变量除非显式的以 local 修饰词定义为局部变量,否则都被定义为全局变量,局部变量作用范围为函数或者代码块内。说明,在变量的首次赋值之前,变量的值均为 nil。– 行注释–[[块注释–]]globalVar = ‘is global’– if代码块if 1 > 0 then local localVar = ‘is local’ print(localVar) – 可以访问局部变量 print(globalVar) – 可以访问全局变量endprint(localVar) – 不能访问局部变量print(globalVar) – 可以访问全局变量标识符约定Lua 中用到的名字(标识符)可以是任何非数字开头的字母、数字、下划线组成的字符串,同大多数语言保持一致。关键字下面这些是保留的关键字,不能用作名字:大部分的流程控制关键字将在 流程控制 部分说明。操作符大部分运算操作符将在 表达式 部分进行说明。语句Lua 的一个执行单元叫做 chunk(语句组),一个语句组就是一串语句段,而 block(语句块)是一列语句段。do block end下面将介绍 Lua 的主要流程控制语句。条件语句Lua 中同样是用 if 语句作为条件流程控制语句,else if 或者 else 子句可以省略。– exp为条件表达式,block为条件语句if exp then blockelseif exp then blockelse blockend控制结构中的条件表达式可以返回任何值。 false 和 nil 都被认为是假,所有其它值都被认为是真。另外 Lua 中并没有提供 switch 子句,我们除了使用冗长的 if 子句外,怎么实现其他语言中的 switch 功能呢?– 利用表实现local switch = { [1] = function() – 索引对应的域为匿名函数 return “Case 1.” end, [2] = function() return “Case 2.” end, [3] = function() return “Case 3.” end}local exp = 4 – exp为条件表达式local func = switch[exp]– 实现switch-default功能if (func) then return func()else return “Case default.“end循环语句Lua 支持 for、while、repeat 这三种循环子句。while 子句结构定义为:– 结束条件为:循环条件==falsewhile 循环条件 do 代码块end– 1+…+10的和local sum = 0local i = 1while i <= 10 do i = i + 1 sum = sum + iendreturn sumfor 子句结构定义为: – 结束条件为:变量<=循环结束值 for 变量=初值, 循环结束值, 步长 do 代码块end– 1+…+10的和local sum = 0for i=1, 10, 1 do sum = sum + iendreturn sum另外,for 结合 in 关键字可以遍历 table 类型的数据,如下:local names = {‘fhb’, ’lw’, ’lbf’}local name;for i,value in ipairs(names) do if i == 1 then name = value endendreturn namerepeat 子句只有循环条件为 true 时,才退出循环。跟通常使用习惯相反,因此使用较少。其结构定义为:– 结束条件为:循环条件==truerepeat 代码块until 循环条件– 1+…+10的和local sum = 0local i = 1repeat i = i + 1 sum = sum + iuntil i > 10return sum语句的退出return 和 break 关键字都可以用来退出语句组,但 return 关键字可以用来退出函数和代码块,包括循环语句,而 break 关键字只能退出循环语句。表达式在 Lua 中由多个操作符和操作数组成一个表达式。赋值Lua 允许多重赋值。 因此,赋值的语法定义是等号左边是一系列变量, 而等号右边是一系列的表达式。 两边的元素都用逗号间。如果右值比需要的更多,多余的值就被忽略,如果右值的数量不够, 将会被扩展若干个 nil。– 变量简单赋值x = 10y = 20– 交换x和y的值x, y = y, x数学运算Lua 支持常见的数学运算操作符,见下表:操作符含义示例+-加减运算10 - 5*/乘除运算10 * 5%取模运算10 % 5^求幂运算4^(-0.5)-取负运算-0.5需要指出的是,string 类型进行数学运算操作时,会隐式转化为 number 类型。return ‘12’ / 6 – 返回2比较运算Lua 中的比较操作符有见下表:操作符含义示例==等于,为严格判断"1” == 1 结果为 false~=不等于等价于==操作的反值"1”~=1 结果为 true<<=小于或小于等于1<=2>>=大于或大于等于2>=1比较运算的结果一定是 boolean 类型。如果操作数都是数字,那么就直接做数字比较,如果操作数都是字符串,就用字符串比较的方式进行,否则,无法进行比较运算。逻辑运算Lua 中的逻辑操作符有 and、or 以及 not,一样把 false 和 nil 都作为假, 而其它值都当作真。操作符含义示例and与10 and 20or或10 or 20not取非not false取反操作 not 总是返回 false 或 true 中的一个。 and 和 or 都遵循短路规则,也就是说 and 操作符在第一个操作数为 false 或 nil 时,返回这第一个操作数, 否则,and 返回第二个参数; or 操作符在第一个操作数不为 nil 和 false 时,返回这第一个操作数,否则返回第二个操作数。10 and 20 –> 20nil and 10 –> nil10 or 20 –> 10nil or “a” –> “a"not false –> true其他运算Lua 中还有两种特别的操作符,分别为字符串连接操作符(..)和取长度操作符(#)。特别说明:如果字符串连接操作符的操作数存在 number 类型,则会隐式转化为 string 类型取长度操作符获取字符串的长度是它的字节数,table 的长度被定义成一个整数下标 n'1’ .. 2 –> ‘12’#‘123’ –> 3#{1, 2} –> 2操作符优先级Lua 中操作符的优先级见下表,从低到高优先级顺序: 运算符优先级通常是这样,但是可以用括号来改变运算次序。函数在 Lua 中,函数是和字符串、数值和表并列的基本数据结构, 属于第一类对象( first-class-object),可以和数值等其他类型一样赋给变量以及作为参数传递,同样可以作为返回值接收(闭包)。定义函数函数在 Lua 中定义也很简单,基本结构为:– arg为参数列表function function_name(arg) bodyend– 阶乘函数function fact(n) if n == 1 then return 1 else return n * fact(n - 1) endend– 调用函数return fact(4)可以用 local 关键字来修饰函数,表示局部函数。local function foo(n) return n * 2end在 Lua 中有一个概念,函数与所有类型值一样都是匿名的,即它们都没有名称。当讨论一个函数名时,实际上是在讨论一个持有某函数的变量:function f(x) return -x end– 上述写法只是一种语法糖,是下述代码的简写形式f = function(x) return -x end函数参数Lua 中函数实参有两种传递方式,但大部分情况会进行值传递。值传递当实参值为非 table 类型时,会采用值传递。几个传参规则如下:若实参个数大于形参个数,从左向右,多余的实参被忽略若实参个数小于形参个数,从左向右,没有被初始化的形参被初始化为 nil支持边长参数,用…表示– 定义两个函数function f(a, b) endfunction g(a, …) end– 调用参数情况f(3) a=3, b=nilf(3, 4, 5) a=3, b=4g(3, 4, 5) a=3, … –> 4 5当函数为变长参数时,函数内使用…来获取变长参数,Lua 5.0 后…替换为名 arg 的隐含局部变量。function f(…) for k,v in ipairs({…}) do print(k, v) endendf(2,3,3) 引用传递当实参为 table 类型时,传递的只是实参的引用而已。local function f(arg) arg[3] = ’new’endlocal a = {1, 2}f(a)return a[3] –> “new"函数返回值Lua 函数允许返回多个值,中间用逗号隔开。函数返回值接收规则:若返回值个数大于接收变量的个数,多余的返回值会被忽略若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nilfunction f1() return “a” endfunction f2() return “a”, “b” endx, y = f1() –> x=“a”, y=nilx = f2() –> x=“a”, “b"被丢弃– table构造式可以接受函数所有返回值local tab = {f2()} –> t={“a”, “b”}– ()会迫使函数返回一个结果printf((f2())) –> “a"Lua 中除了我们自定义函数外,已经实现了部分功能函数,见 标准函数库。表定义和使用Lua 中最特别的数据类型就是表(table),可以用来实现数组、Hash、对象,全局变量也使用表来管理。– arraylocal array = { 1, 2, 3 }print(array[1], #array) –> 1, 3– hashlocal hash = { a=1, b=2, c=3 }print(hash.a, hash[‘b’], #hash) –> 1, 2, 0– array和hashlocal tab = {1, 2, 3}tab[‘x’] = function() return ‘hash’ endreturn {tab.x, #tab} –> 2, 3说明:当表表示数组时,索引从 1 开始。元表元表(metatable)中的键名称为事件,值称为元方法,它用来定义原始值在特定操作下的行为。可通过 getmetatable() 来获取任一事件的元方法,同样可以通过 setmetatable() 覆盖任一事件的元方法。Lua 支持的表事件:元方法事件__add(table, value)__sub(table, value)+ 和 - 操作__mul(table, value)__div(table, value)* 和 / 操作__mod(table, value)__pow(table, value)% 和 ^ 操作__concat(table, value).. 操作__len(table)# 操作__eq(table, value)__lt(table, value)__le(table, value)== 、<、<= 操作__index(table, index)__newindex(table, index)取和赋值下标操作__call(table, …)调用一个值__tostring(table)调用 tostring() 时覆盖这些元方法,即可实现重载运算符操作。例如重载 tostring 事件:local hash = { x = 2, y = 3 }local operator = { __tostring = function(self) return “{ " .. self.x .. “, " .. self.y .. " }” end}setmetatable(hash, operator)print(tostring(hash)) –> “{ 2, 3 }“总结Lua 是面向过程语言,使得可以简单易学。轻量级的特性,使得以脚本方式轻易地嵌入别的程序中,例如 PHP、JAVA、Redis、Nginx 等语言或应用。当然,Lua 也可以通过表实现面向对象编程。相关文章 »Lua在Redis的应用(2017-09-04)Lua在Nginx的应用(2017-09-09) ...

March 25, 2019 · 3 min · jiezi

当 Go 遇上了 Lua

在 GitHub 玩耍时,偶然发现了 gopher-lua ,这是一个纯 Golang 实现的 Lua 虚拟机。我们知道 Golang 是静态语言,而 Lua 是动态语言,Golang 的性能和效率各语言中表现得非常不错,但在动态能力上,肯定是无法于 Lua 相比。那么如果我们能够将二者结合起来,就能综合二者各自的长处了(手动滑稽。在项目 Wiki 中,我们可以知道 gopher-lua 的执行效率和性能仅比 C 实现的 bindings 差。因此从性能方面考虑,这应该是一款非常不错的虚拟机方案。Hello World这里给出了一个简单的 Hello World 程序。我们先是新建了一个虚拟机,随后对其进行了 DoString(…) 解释执行 lua 代码的操作,最后将虚拟机关闭。执行程序,我们将在命令行看到 “Hello World” 的字符串。package mainimport ( “github.com/yuin/gopher-lua”)func main() { l := lua.NewState() defer l.Close() if err := l.DoString(print("Hello World")); err != nil { panic(err) }}// Hello World提前编译在查看上述 DoString(…) 方法的调用链后,我们发现每执行一次 DoString(…) 或 DoFile(…) ,都会各执行一次 parse 和 compile 。func (ls *LState) DoString(source string) error { if fn, err := ls.LoadString(source); err != nil { return err } else { ls.Push(fn) return ls.PCall(0, MultRet, nil) }}func (ls *LState) LoadString(source string) (*LFunction, error) { return ls.Load(strings.NewReader(source), “<string>”)}func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) { chunk, err := parse.Parse(reader, name) // … proto, err := Compile(chunk, name) // …}从这一点考虑,在同份 Lua 代码将被执行多次(如在 http server 中,每次请求将执行相同 Lua 代码)的场景下,如果我们能够对代码进行提前编译,那么应该能够减少 parse 和 compile 的开销(如果这属于 hotpath 代码)。根据 Benchmark 结果,提前编译确实能够减少不必要的开销。package glua_testimport ( “bufio” “os” “strings” lua “github.com/yuin/gopher-lua” “github.com/yuin/gopher-lua/parse”)// 编译 lua 代码字段func CompileString(source string) (*lua.FunctionProto, error) { reader := strings.NewReader(source) chunk, err := parse.Parse(reader, source) if err != nil { return nil, err } proto, err := lua.Compile(chunk, source) if err != nil { return nil, err } return proto, nil}// 编译 lua 代码文件func CompileFile(filePath string) (*lua.FunctionProto, error) { file, err := os.Open(filePath) defer file.Close() if err != nil { return nil, err } reader := bufio.NewReader(file) chunk, err := parse.Parse(reader, filePath) if err != nil { return nil, err } proto, err := lua.Compile(chunk, filePath) if err != nil { return nil, err } return proto, nil}func BenchmarkRunWithoutPreCompiling(b *testing.B) { l := lua.NewState() for i := 0; i < b.N; i++ { _ = l.DoString(a = 1 + 1) } l.Close()}func BenchmarkRunWithPreCompiling(b *testing.B) { l := lua.NewState() proto, _ := CompileString(a = 1 + 1) lfunc := l.NewFunctionFromProto(proto) for i := 0; i < b.N; i++ { l.Push(lfunc) _ = l.PCall(0, lua.MultRet, nil) } l.Close()}// goos: darwin// goarch: amd64// pkg: glua// BenchmarkRunWithoutPreCompiling-8 100000 19392 ns/op 85626 B/op 67 allocs/op// BenchmarkRunWithPreCompiling-8 1000000 1162 ns/op 2752 B/op 8 allocs/op// PASS// ok glua 3.328s虚拟机实例池在同份 Lua 代码被执行的场景下,除了可使用提前编译优化性能外,我们还可以引入虚拟机实例池。因为新建一个 Lua 虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。func BenchmarkRunWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { l := lua.NewState() _ = l.DoString(a = 1 + 1) l.Close() }}func BenchmarkRunWithPool(b *testing.B) { pool := newVMPool(nil, 100) for i := 0; i < b.N; i++ { l := pool.get() _ = l.DoString(a = 1 + 1) pool.put(l) }}// goos: darwin// goarch: amd64// pkg: glua// BenchmarkRunWithoutPool-8 10000 129557 ns/op 262599 B/op 826 allocs/op// BenchmarkRunWithPool-8 100000 19320 ns/op 85626 B/op 67 allocs/op// PASS// ok glua 3.467sBenchmark 结果显示,虚拟机实例池的确能够减少很多内存分配操作。下面给出了 README 提供的实例池实现,但注意到该实现在初始状态时,并未创建足够多的虚拟机实例(初始时,实例数为0),以及存在 slice 的动态扩容问题,这都是值得改进的地方。type lStatePool struct { m sync.Mutex saved []*lua.LState}func (pl *lStatePool) Get() *lua.LState { pl.m.Lock() defer pl.m.Unlock() n := len(pl.saved) if n == 0 { return pl.New() } x := pl.saved[n-1] pl.saved = pl.saved[0 : n-1] return x}func (pl *lStatePool) New() *lua.LState { L := lua.NewState() // setting the L up here. // load scripts, set global variables, share channels, etc… return L}func (pl *lStatePool) Put(L *lua.LState) { pl.m.Lock() defer pl.m.Unlock() pl.saved = append(pl.saved, L)}func (pl *lStatePool) Shutdown() { for _, L := range pl.saved { L.Close() }}// Global LState poolvar luaPool = &lStatePool{ saved: make([]*lua.LState, 0, 4),}模块调用gopher-lua 支持 Lua 调用 Go 模块,个人觉得,这是一个非常令人振奋的功能点,因为在 Golang 程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。当然,除此之外,也存在 Go 调用 Lua 模块,但个人感觉后者是没啥必要的,所以在这里并没有涉及后者的内容。package mainimport ( “fmt” lua “github.com/yuin/gopher-lua”)const source = local m = require("gomodule")m.goFunc()print(m.name)func main() { L := lua.NewState() defer L.Close() L.PreloadModule(“gomodule”, load) if err := L.DoString(source); err != nil { panic(err) }}func load(L *lua.LState) int { mod := L.SetFuncs(L.NewTable(), exports) L.SetField(mod, “name”, lua.LString(“gomodule”)) L.Push(mod) return 1}var exports = map[string]lua.LGFunction{ “goFunc”: goFunc,}func goFunc(L *lua.LState) int { fmt.Println(“golang”) return 0}// golang// gomodule变量污染当我们使用实例池减少开销时,会引入另一个棘手的问题:由于同一个虚拟机可能会被多次执行同样的 Lua 代码,进而变动了其中的全局变量。如果代码逻辑依赖于全局变量,那么可能会出现难以预测的运行结果(这有点数据库隔离性中的“不可重复读”的味道)。全局变量如果我们需要限制 Lua 代码只能使用局部变量,那么站在这个出发点上,我们需要对全局变量做出限制。那问题来了,该如何实现呢?我们知道,Lua 是编译成字节码,再被解释执行的。那么,我们可以在编译字节码的阶段中,对全局变量的使用作出限制。在查阅完 Lua 虚拟机指令后,发现涉及到全局变量的指令有两条:GETGLOBAL(Opcode 5)和 SETGLOBAL(Opcode 7)。到这里,已经有了大致的思路:我们可通过判断字节码是否含有 GETGLOBAL 和 SETGLOBAL 进而限制代码的全局变量的使用。至于字节码的获取,可通过调用 CompileString(…) 和 CompileFile(…) ,得到 Lua 代码的 FunctionProto ,而其中的 Code 属性即为字节码 slice,类型为 []uint32 。在虚拟机实现代码中,我们可以找到一个根据字节码输出对应 OpCode 的工具函数。// 获取对应指令的 OpCodefunc opGetOpCode(inst uint32) int { return int(inst >> 26)}有了这个工具函数,我们即可实现对全局变量的检查。package main// …func CheckGlobal(proto *lua.FunctionProto) error { for _, code := range proto.Code { switch opGetOpCode(code) { case lua.OP_GETGLOBAL: return errors.New(“not allow to access global”) case lua.OP_SETGLOBAL: return errors.New(“not allow to set global”) } } // 对嵌套函数进行全局变量的检查 for _, nestedProto := range proto.FunctionPrototypes { if err := CheckGlobal(nestedProto); err != nil { return err } } return nil}func TestCheckGetGlobal(t *testing.T) { l := lua.NewState() proto, _ := CompileString(print(_G)) if err := CheckGlobal(proto); err == nil { t.Fail() } l.Close()}func TestCheckSetGlobal(t *testing.T) { l := lua.NewState() proto, _ := CompileString(_G = {}) if err := CheckGlobal(proto); err == nil { t.Fail() } l.Close()}模块除变量可能被污染外,导入的 Go 模块也有可能在运行期间被篡改。因此,我们需要一种机制,确保导入到虚拟机的模块不被篡改,即导入的对象是只读的。在查阅相关博客后,我们可以对 Table 的 __newindex 方法的修改,将模块设置为只读模式。package mainimport ( “fmt” “github.com/yuin/gopher-lua”)// 设置表为只读func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData { ud := l.NewUserData() mt := l.NewTable() // 设置表中域的指向为 table l.SetField(mt, “__index”, table) // 限制对表的更新操作 l.SetField(mt, “__newindex”, l.NewFunction(func(state *lua.LState) int { state.RaiseError(“not allow to modify table”) return 0 })) ud.Metatable = mt return ud}func load(l *lua.LState) int { mod := l.SetFuncs(l.NewTable(), exports) l.SetField(mod, “name”, lua.LString(“gomodule”)) // 设置只读 l.Push(SetReadOnly(l, mod)) return 1}var exports = map[string]lua.LGFunction{ “goFunc”: goFunc,}func goFunc(l *lua.LState) int { fmt.Println(“golang”) return 0}func main() { l := lua.NewState() l.PreloadModule(“gomodule”, load) // 尝试修改导入的模块 if err := l.DoString(local m = require("gomodule");m.name = "hello world"); err != nil { fmt.Println(err) } l.Close()}// <string>:1: not allow to modify table写在最后Golang 和 Lua 的融合,开阔了我的视野:原来静态语言和动态语言还能这么融合,静态语言的运行高效率,配合动态语言的开发高效率,想想都兴奋(逃。在网上找了很久,发现并没有关于 Go-Lua 的技术分享,只找到了一篇稍微有点联系的文章(京东三级列表页持续架构优化 — Golang + Lua (OpenResty) 最佳实践),且在这篇文章中, Lua 还是跑在 C 上的。由于信息的缺乏以及本人(学生党)开发经验不足的原因,并不能很好地评价该方案在实际生产中的可行性。因此,本篇文章也只能当作“闲文”了,哈哈。参考资料深入浅出Lua虚拟机A No-Frills Introduction to Lua 5.1 VM Instructionscocos2d-lua disable unexpected global variablelua中设置只读tableMetableEventsgithub.com/zhu327/gluaor ...

March 13, 2019 · 5 min · jiezi

网关 rate limit 网络速率限制方案

网关 rate limit 网络速率限制方案一、网络限流算法 在计算机领域中,限流技术(time limiting)被用来控制网络接口收发通讯数据的速率。用这个方法来优化性能、较少延迟和提高带宽等。 在互联网领域中也借鉴了这个概念,用来控制网络请求的速率,在高并发,大流量的场景中,比如双十一秒杀、抢购、抢票、抢单等场景。 网络限流主流的算法有两种,分别是漏桶算法和令牌桶算法。接下来我们一一为大家介绍:1. 漏桶算法描述:漏桶算法思路很简单,水(数据或者请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。实现逻辑: 控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 优缺点:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法可以结合起来为网络流量提供更大的控制。2. 令牌桶算法实现逻辑:令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 令牌桶的另外一个好处是可以方便的改变速度。 一旦需要提高速率,则按需提高放入桶中的令牌的速率。 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量, 比如华为的专利"采用令牌漏桶进行报文限流的方法"(CN 1536815 A),提供了一种动态计算可用令牌数的方法, 相比其它定时增加令牌的方法, 它只在收到一个报文后,计算该报文与前一报文到来的时间间隔内向令牌漏桶内注入的令牌数, 并计算判断桶内的令牌数是否满足传送该报文的要求。二、常见的 Rate limiting 实现方式通常意义上的限速,其实可以分为以下三种:limit_rate 限制响应速度limit_conn 限制连接数limit_req 限制请求数1. Nginx 模块 (漏桶)参考地址:limit_req_modulengx_http_limit_req_module模块(0.7.21)用于限制每个定义键的请求处理速度,特别是来自单个IP地址的请求的处理速度。1.1 Example Configurationhttp { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; … server { … location /search/ { limit_req zone=one burst=5; }1.2 使用规则语法: limit_req zone=name [burst=number] [nodelay | delay=number];默认: —作用范围: http, server, location参数说明zone 设置内存名称和内存大小。burst 漏桶的突发大小。当大于突发值是请求被延迟。nodelay|delay delay参数(1.15.7)指定了过度请求延迟的限制。默认值为零,即所有过量的请求都被延迟。设置共享内存区域和请求的最大突发大小。如果请求速率超过为区域配置的速率,则延迟处理请求,以便以定义的速率处理请求。过多的请求会被延迟,直到它们的数量超过最大突发大小,在这种情况下,请求会因错误而终止。默认情况下,最大突发大小等于零。limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;server { location /search/ { limit_req zone=one burst=5; }描述:平均每秒不允许超过一个请求,突发请求不超过5个。—–参数使用说明—–如果不希望在请求受到限制时延迟过多的请求,则应使用参数nodelay:limit_req zone=one burst=5 nodelay;可以有几个limit_req指令。例如,下面的配置将限制来自单个IP地址的请求的处理速度,同时限制虚拟服务器的请求处理速度:limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;limit_req_zone $server_name zone=perserver:10m rate=10r/s;server { … limit_req zone=perip burst=5 nodelay; limit_req zone=perserver burst=10;}当且仅当当前级别上没有limit_req指令时,这些指令从上一级继承。1.3 围绕limit_req_zone的相关配置语法: limit_req_log_level info | notice | warn | error;默认: limit_req_log_level error;作用范围: http, server, locationThis directive appeared in version 0.8.18.设置所需的日志记录级别,用于服务器因速率超过或延迟请求处理而拒绝处理请求的情况。延迟日志记录级别比拒绝日志记录级别低1点;例如,如果指定了“limit_req_log_level通知”,则使用info级别记录延迟。错误状态语法: limit_req_status code;默认: limit_req_status 503;作用范围: http, server, locationThis directive appeared in version 1.3.15.设置状态代码以响应被拒绝的请求。语法: limit_req_zone key zone=name:size rate=rate [sync];默认: —作用范围: http设置共享内存区域的参数,该区域将保存各种键的状态。特别是,状态存储当前过多请求的数量。键可以包含文本、变量及其组合。键值为空的请求不被计算。Prior to version 1.7.6, a key could contain exactly one variable.例如:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;说明:在这里,状态保存在一个10mb的区域“1”中,该区域的平均请求处理速度不能超过每秒1个请求。总结:客户端IP地址作为密钥。注意,这里使用的是$binary_remote_addr变量,而不是$remote_addr。$binary_remote_addr变量的大小对于IPv4地址总是4个字节,对于IPv6地址总是16个字节。存储状态在32位平台上总是占用64字节,在64位平台上占用128字节。一个兆字节区域可以保存大约16000个64字节的状态,或者大约8000个128字节的状态。如果区域存储耗尽,则删除最近最少使用的状态。即使在此之后无法创建新状态,请求也会因错误而终止。速率以每秒请求数(r/s)指定。如果需要每秒少于一个请求的速率,则在每分钟请求(r/m)中指定。例如,每秒半请求是30r/m。2. Openresty 模块参考地址:lua-resty-limit-traffic参考地址:openresty常用限速2.1 限制接口总并发数按照 ip 限制其并发连接数lua_shared_dict my_limit_conn_store 100m;…location /hello { access_by_lua_block { local limit_conn = require “resty.limit.conn” – 限制一个 ip 客户端最大 1 个并发请求 – burst 设置为 0,如果超过最大的并发请求数,则直接返回503, – 如果此处要允许突增的并发数,可以修改 burst 的值(漏桶的桶容量) – 最后一个参数其实是你要预估这些并发(或者说单个请求)要处理多久,以便于对桶里面的请求应用漏桶算法 local lim, err = limit_conn.new(“my_limit_conn_store”, 1, 0, 0.5) if not lim then ngx.log(ngx.ERR, “failed to instantiate a resty.limit.conn object: “, err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr – commit 为true 代表要更新shared dict中key的值, – false 代表只是查看当前请求要处理的延时情况和前面还未被处理的请求数 local delay, err = lim:incoming(key, true) if not delay then if err == “rejected” then return ngx.exit(503) end ngx.log(ngx.ERR, “failed to limit req: “, err) return ngx.exit(500) end – 如果请求连接计数等信息被加到shared dict中,则在ctx中记录下, – 因为后面要告知连接断开,以处理其他连接 if lim:is_committed() then local ctx = ngx.ctx ctx.limit_conn = lim ctx.limit_conn_key = key ctx.limit_conn_delay = delay end local conn = err – 其实这里的 delay 肯定是上面说的并发处理时间的整数倍, – 举个例子,每秒处理100并发,桶容量200个,当时同时来500个并发,则200个拒掉 – 100个在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.5秒处理, – 101-200个则应该延后0.5*2=1秒处理(0.5是上面预估的并发处理时间) if delay >= 0.001 then ngx.sleep(delay) end } log_by_lua_block { local ctx = ngx.ctx local lim = ctx.limit_conn if lim then local key = ctx.limit_conn_key – 这个连接处理完后应该告知一下,更新shared dict中的值,让后续连接可以接入进来处理 – 此处可以动态更新你之前的预估时间,但是别忘了把limit_conn.new这个方法抽出去写, – 要不每次请求进来又会重置 local conn, err = lim:leaving(key, 0.5) if not conn then ngx.log(ngx.ERR, “failed to record the connection leaving “, “request: “, err) return end end } proxy_pass http://10.100.157.198:6112; proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 60; proxy_read_timeout 600; proxy_send_timeout 600;}说明:其实此处没有设置 burst 的值,就是单纯的限制最大并发数,如果设置了 burst 的值,并且做了延时处理,其实就是对并发数使用了漏桶算法,但是如果不做延时处理,其实就是使用的令牌桶算法。参考下面对请求数使用漏桶令牌桶的部分,并发数的漏桶令牌桶实现与之相似2.2 限制接口时间窗请求数限制 ip 每分钟只能调用 120 次 /hello 接口(允许在时间段开始的时候一次性放过120个请求)lua_shared_dict my_limit_count_store 100m;…init_by_lua_block { require “resty.core”}….location /hello { access_by_lua_block { local limit_count = require “resty.limit.count” – rate: 10/min local lim, err = limit_count.new(“my_limit_count_store”, 120, 60) if not lim then ngx.log(ngx.ERR, “failed to instantiate a resty.limit.count object: “, err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) – 如果请求数在限制范围内,则当前请求被处理的延迟(这种场景下始终为0,因为要么被处理要么被拒绝)和将被处理的请求的剩余数 if not delay then if err == “rejected” then return ngx.exit(503) end ngx.log(ngx.ERR, “failed to limit count: “, err) return ngx.exit(500) end } proxy_pass http://10.100.157.198:6112; proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 60; proxy_read_timeout 600; proxy_send_timeout 600;}2.3 平滑限制接口请求数限制 ip 每分钟只能调用 120 次 /hello 接口(平滑处理请求,即每秒放过2个请求)lua_shared_dict my_limit_req_store 100m;….location /hello { access_by_lua_block { local limit_req = require “resty.limit.req” – 这里设置rate=2/s,漏桶桶容量设置为0,(也就是来多少水就留多少水) – 因为resty.limit.req代码中控制粒度为毫秒级别,所以可以做到毫秒级别的平滑处理 local lim, err = limit_req.new(“my_limit_req_store”, 2, 0) if not lim then ngx.log(ngx.ERR, “failed to instantiate a resty.limit.req object: “, err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == “rejected” then return ngx.exit(503) end ngx.log(ngx.ERR, “failed to limit req: “, err) return ngx.exit(500) end } proxy_pass http://10.100.157.198:6112; proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 60; proxy_read_timeout 600; proxy_send_timeout 600;}2.4 漏桶算法限流限制 ip 每分钟只能调用 120 次 /hello 接口(平滑处理请求,即每秒放过2个请求),超过部分进入桶中等待,(桶容量为60),如果桶也满了,则进行限流lua_shared_dict my_limit_req_store 100m;….location /hello { access_by_lua_block { local limit_req = require “resty.limit.req” – 这里设置rate=2/s,漏桶桶容量设置为0,(也就是来多少水就留多少水) – 因为resty.limit.req代码中控制粒度为毫秒级别,所以可以做到毫秒级别的平滑处理 local lim, err = limit_req.new(“my_limit_req_store”, 2, 60) if not lim then ngx.log(ngx.ERR, “failed to instantiate a resty.limit.req object: “, err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == “rejected” then return ngx.exit(503) end ngx.log(ngx.ERR, “failed to limit req: “, err) return ngx.exit(500) end – 此方法返回,当前请求需要delay秒后才会被处理,和他前面对请求数 – 所以此处对桶中请求进行延时处理,让其排队等待,就是应用了漏桶算法 – 此处也是与令牌桶的主要区别既 if delay >= 0.001 then ngx.sleep(delay) end } proxy_pass http://10.100.157.198:6112; proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 60; proxy_read_timeout 600; proxy_send_timeout 600;}3.5 令牌桶算法限流限制 ip 每分钟只能调用 120 次 /hello 接口(平滑处理请求,即每秒放过2个请求),但是允许一定的突发流量(突发的流量,就是桶的容量(桶容量为60),超过桶容量直接拒绝lua_shared_dict my_limit_req_store 100m;….location /hello { access_by_lua_block { local limit_req = require “resty.limit.req” local lim, err = limit_req.new(“my_limit_req_store”, 2, 0) if not lim then ngx.log(ngx.ERR, “failed to instantiate a resty.limit.req object: “, err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == “rejected” then return ngx.exit(503) end ngx.log(ngx.ERR, “failed to limit req: “, err) return ngx.exit(500) end – 此方法返回,当前请求需要delay秒后才会被处理,和他前面对请求数 – 此处忽略桶中请求所需要的延时处理,让其直接返送到后端服务器, – 其实这就是允许桶中请求作为突发流量 也就是令牌桶桶的原理所在 if delay >= 0.001 then – ngx.sleep(delay) end } proxy_pass http://10.100.157.198:6112; proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 60; proxy_read_timeout 600; proxy_send_timeout 600;}说明:其实nginx的ngx_http_limit_req_module 这个模块中的delay和nodelay也就是类似此处对桶中请求是否做延迟处理的两种方案,也就是分别对应的漏桶和令牌桶两种算法注意:resty.limit.traffic 模块说明 This library is already usable though still highly experimental. 意思是说目前这个模块虽然可以使用了,但是还处在高度实验性阶段,所以目前(2019-03-11)放弃使用resty.limit.traffic模块。3. kong 插件参考地址:Rate Limiting Advanced (企业版)参考地址:request-termination参考地址:rate-limiting 请求限速参考地址:request-size-limiting (官方建议开启此插件,防止DOS(拒绝服务)攻击)参考地址:response-ratelimiting 响应限速参考地址:kong-response-size-limiting (非官方提供)3.1 rate-limiting速率限制开发人员在给定的几秒、几分钟、几小时、几天、几个月或几年时间内可以发出多少HTTP请求。如果底层服务/路由(或废弃的API实体)没有身份验证层,那么将使用客户机IP地址,否则,如果配置了身份验证插件,将使用使用者。在一个Service上启用该插件$ curl -X POST http://kong:8001/services/{service}/plugins \ –data “name=rate-limiting” \ –data “config.second=5” \ –data “config.hour=10000"在一个router上启用该插件$ curl -X POST http://kong:8001/routes/{route_id}/plugins \ –data “name=rate-limiting” \ –data “config.second=5” \ –data “config.hour=10000"在一个consumer上启动该插件$ curl -X POST http://kong:8001/plugins \ –data “name=rate-limiting” \ –data “consumer_id={consumer_id}” \ –data “config.second=5” \ –data “config.hour=10000"rate-limiting支持三个策略,它们分别拥有自己的优缺点策略优点缺点cluster准确,没有额外的组件来支持相对而言,性能影响最大的是,每个请求都强制对底层数据存储执行读和写操作。redis准确,比集群策略对性能的影响更小额外的redis安装要求,比本地策略更大的性能影响local最小的性能影响不太准确,除非在Kong前面使用一致哈希负载均衡器,否则在扩展节点数量时它会发散3.2 response-ratelimiting此插件允许您根据上游服务返回的自定义响应头限制开发人员可以发出的请求数量。您可以任意设置任意数量的限速对象(或配额),并指示Kong按任意数量增加或减少它们。每个自定义速率限制对象都可以限制每秒、分钟、小时、天、月或年的入站请求。在一个Service上启用该插件$ curl -X POST http://kong:8001/services/{service}/plugins \ –data “name=response-ratelimiting” \ –data “config.limits.{limit_name}=” \ –data “config.limits.{limit_name}.minute=10"在一个router上启用该插件$ curl -X POST http://kong:8001/routes/{route_id}/plugins \ –data “name=response-ratelimiting” \ –data “config.limits.{limit_name}=” \ –data “config.limits.{limit_name}.minute=10"在一个consumer上启动该插件$ curl -X POST http://kong:8001/plugins \ –data “name=response-ratelimiting” \ –data “consumer_id={consumer_id}” \ –data “config.limits.{limit_name}=” \ –data “config.limits.{limit_name}.minute=10"在api上启用该插件$ curl -X POST http://kong:8001/apis/{api}/plugins \ –data “name=response-ratelimiting” \ –data “config.limits.{limit_name}=” \ –data “config.limits.{limit_name}.minute=10"3.3 request-size-limiting阻塞体大于特定大小(以兆为单位)的传入请求。在一个Service上启用该插件$ curl -X POST http://kong:8001/services/{service}/plugins \ –data “name=request-size-limiting” \ –data “config.allowed_payload_size=128"在一个router上启用该插件$ curl -X POST http://kong:8001/routes/{route_id}/plugins \ –data “name=request-size-limiting” \ –data “config.allowed_payload_size=128"在一个consumer上启动该插件$ curl -X POST http://kong:8001/plugins \ –data “name=request-size-limiting” \ –data “consumer_id={consumer_id}” \ –data “config.allowed_payload_size=128"3.4 request-termination此插件使用指定的状态代码和消息终止传入的请求。这允许(暂时)停止服务或路由上的通信,甚至阻塞消费者。在一个Service上启用该插件$ curl -X POST http://kong:8001/services/{service}/plugins \ –data “name=request-termination” \ –data “config.status_code=403” \ –data “config.message=So long and thanks for all the fish!“在一个router上启用该插件$ curl -X POST http://kong:8001/routes/{route_id}/plugins \ –data “name=request-termination” \ –data “config.status_code=403” \ –data “config.message=So long and thanks for all the fish!“在一个consumer上启动该插件$ curl -X POST http://kong:8001/plugins \ –data “name=request-termination” \ –data “consumer_id={consumer_id}” \ –data “config.status_code=403” \ –data “config.message=So long and thanks for all the fish!“4. 基于redis - INCR key参考地址:pattern-rate-limiter(翻墙)使用redis的INCR key,它的意思是将存储在key上的值加1。如果key不存在,在操作之前将值设置为0。如果键包含错误类型的值或包含不能表示为整数的字符串,则返回错误。此操作仅限于64位带符号整数。return value Integer reply: the value of key after the incrementexamplesredis> SET mykey “10"“OK"redis> INCR mykey(integer) 11redis> GET mykey"11"redis> INCR key 有两种用法:计数器(counter),比如文章浏览总量、分布式数据分页、游戏得分等;限速器(rate limiter),速率限制器模式是一种特殊的计数器,用于限制操作的执行速率,比如:限制可以针对公共API执行的请求数量;本方案的重点是使用redis实现一个限速器,我们使用INCR提供了该模式的两种实现,其中我们假设要解决的问题是将API调用的数量限制在每IP地址每秒最多10个请求:第一种方式,基本上每个IP都有一个计数器,每个不同的秒都有一个计数器FUNCTION LIMIT_API_CALL(ip)ts = CURRENT_UNIX_TIME()keyname = ip+”:"+tscurrent = GET(keyname)IF current != NULL AND current > 10 THEN ERROR “too many requests per second"ELSE MULTI INCR(keyname,1) EXPIRE(keyname,10) EXEC PERFORM_API_CALL()END优点:使用ip+ts的方式,确保了每秒的缓存都是不同的key,将每一秒产生的redisobject隔离开。没有使用过期时间强制限制redis过期时效。缺点:会产生大量的redis-key,虽然都写入了过期时间,但是对于redis-key的清理也是一种负担。有可能会影响redis的读性能。第二种方式,创建计数器的方式是,从当前秒中执行的第一个请求开始,它只能存活一秒钟。如果在同一秒内有超过10个请求,计数器将达到一个大于10的值,否则它将过期并重新从0开始。FUNCTION LIMIT_API_CALL(ip):current = GET(ip)IF current != NULL AND current > 10 THEN ERROR “too many requests per second"ELSE value = INCR(ip) IF value == 1 THEN EXPIRE(ip,1) END PERFORM_API_CALL()END优点:相对于方案一种占用空间更小,执行效率更高。缺点:INCR命令和EXPIRE命令不是原子操作,存在一个竞态条件。如果由于某种原因客户端执行INCR命令,但没有执行过期,密钥将被泄露,直到我们再次看到相同的IP地址。修复方案:将带有可选过期的INCR转换为使用EVAL命令发送的Lua脚本(只有在Redis 2.6版本中才可用)。使用lua局部变量来解决,保证每次都能设置过期时间。local currentcurrent = redis.call(“incr”,KEYS[1])if tonumber(current) == 1 then redis.call(“expire”,KEYS[1],1)end三、最终实现方案根据几种常见的实现方案和场景以及优缺点最终采用的是使用kong的插件 rate-limiting ,如果不符合要求进行二次开发。直接开发kong插件使用令牌桶+redis实现限流 ...

March 11, 2019 · 6 min · jiezi

使用idea调试lua代码-Openresty

使用idea调试lua代码Openresty是基于nginx与lua的高性能web框架,继承了大量的高质量的lua库、第三方模块以及大多数依赖项。目前对于lua主流开发工具有vscode+lua插件、IntelliJ IDEA+EmmyLua、ZeroBrane Studio、还有其他的一些文本编辑软件等。lua作为一种脚本语言,除了开发简洁,性能优越之外,还应该具备调试功能,对于开发者才能算得上更加友好。本文将使用IntelliJ IDEA+EmmyLua使用远程调试和本地调试。IntelliJ IDEA 2018.2.1Lua 5.1EmmyLua 1.2.6-IDEA182MobDebug 0.70项目目录结构源码位置often-script一、远程调试1、打开idea中调试配置,使用Lua Remote(Mobdebug),如下图:2、配置调试名称和远程调试端口;3、在需要调试的位置加上调试代码;— 启动调试local mobdebug = require(“src.initial.mobdebug”);mobdebug.start();4、启动Openresty项目,然后打开debug模式;5、启动openresty项目;# 进入到工作目录cd /Users/xiaoyueya/projects/vscode/often-script/lua/project# 启动nginxsudo nginx -p ./ -c nginx-debug.conf -s reload6、刷新浏览器;7、断点位置和lua栈信息;8、执行结果;二、本地调试1、打开idea中调试配置,使用lua application ,如下图:2、配置工作目录和执行文件入口;3、编写调试名称为main.lua,然后点击OK,进入主编辑页,找到调试按钮;4、开始本地调试;5、查看堆栈信息;6、查看执行结果

March 8, 2019 · 1 min · jiezi

Nginx 通过 Lua + Redis 实现动态封禁 IP

背景为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。架构实现 IP 黑名单的功能有很多途径:1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。为了方便管理和共享,我们选择通过 Nginx+Lua+Redis 的架构实现 IP 黑名单的功能,架构图如下:实现1、安装 Nginx+Lua模块,推荐使用 OpenResty,这是一个集成了各种 Lua 模块的 Nginx 服务器:2、安装并启动 Redis 服务器;3、配置 Nginx 示例:Nginx 配置其中lua_shared_dict ip_blacklist 1m;由 Nginx 进程分配一块 1M 大小的共享内存空间,用来缓存 IP 黑名单。access_by_lua_file lua/ip_blacklist.lua;指定 lua 脚本位置。4、配置 lua 脚本,定期从 Redis 获取最新的 IP 黑名单。5、在 Redis 服务器上新建 Set 类型的数据 ip_blacklist,并加入最新的 IP 黑名单。完成以上步骤后,重新加载 nginx,配置便开始生效了。这时访问服务器,如果你的 IP 地址在黑名单内的话,将出现拒绝访问,如下图:总结以上,便是 Nginx+Lua+Redis 实现的 IP 黑名单功能,具有如下优点:1、配置简单、轻量,几乎对服务器性能不产生影响;2、多台服务器可以通过Redis实例共享黑名单;3、动态配置,可以手工或者通过某种自动化的方式设置 Redis 中的黑名单。 ...

February 24, 2019 · 1 min · jiezi

后端技术图谱

https://github.com/xingshaoch…

February 23, 2019 · 1 min · jiezi

Redis 中 Lua 脚本的应用和实践

引言前段时间组内有个投票的产品,上线前考虑欠缺,导致被刷票严重。后来,通过研究,发现可以通过 redis lua 脚本实现限流,这里将 redis lua 脚本相关的知识分享出来,讲的不到位的地方还望斧正。redis lua 脚本相关命令这一小节的内容是基本命令,可粗略阅读后跳过,等使用的时候再回来查询redis 自 2.6.0 加入了 lua 脚本相关的命令,EVAL、EVALSHA、SCRIPT EXISTS、SCRIPT FLUSH、SCRIPT KILL、SCRIPT LOAD,自 3.2.0 加入了 lua 脚本的调试功能和命令SCRIPT DEBUG。这里对命令做下简单的介绍。EVAL执行一段lua脚本,每次都需要将完整的lua脚本传递给redis服务器。SCRIPT LOAD将一段lua脚本缓存到redis中并返回一个tag串,并不会执行。EVALSHA执行一个脚本,不过传入参数是「2」中返回的tag,节省网络带宽。SCRIPT EXISTS判断「2」返回的tag串是否存在服务器中。SCRIPT FLUSH清除服务器上的所有缓存的脚本。SCRIPT KILL杀死正在运行的脚本。SCRIPT DEBUG设置调试模式,可设置同步、异步、关闭,同步会阻塞所有请求。生产环境中,推荐使用EVALSHA,相较于EVAL的每次发送脚本主体、浪费带宽,会更高效。这里要注意SCRIPT KILL,杀死正在运行脚本的时候,如果脚本执行过写操作了,这里会杀死失败,因为这违反了 redis lua 脚本的原子性。调试尽量放在测试环境完成之后再发布到生产环境,在生产环境调试千万不要使用同步模式,原因下文会详细讨论。Redis 中 lua 脚本的书写和调试redis lua 脚本是对其现有命令的扩充,单个命令不能完成、需要多个命令,但又要保证原子性的动作可以用脚本来实现。脚本中的逻辑一般比较简单,不要加入太复杂的东西,因为 redis 是单线程的,当脚本执行的时候,其他命令、脚本需要等待直到当前脚本执行完成。因此,对 lua 的语法也不需完全了解,了解基本的使用就足够了,这里对 lua 语法不做过多介绍,会穿插到脚本示例里面。一个秒杀抢购示例假设有一个秒杀活动,商品库存 100,每个用户 uid 只能抢购一次。设计抢购流程如下:先通过 uid 判断是否已经抢过,已经抢过返回0结束。判断商品剩余库存是否大于0,是的话进入「3」,否的话返回0结束。将用户 uid 加入已购用户set中。物品数量减一,返回成功1结束。local goodsSurpluslocal flag– 判断用户是否已抢过local buyMembersKey = tostring(KEYS[1])local memberUid = tonumber(ARGV[1])local goodsSurplusKey = tostring(KEYS[2])local hasBuy = redis.call(“sIsMember”, buyMembersKey, memberUid)– 已经抢购过,返回0if hasBuy ~= 0 then return 0end– 准备抢购goodsSurplus = redis.call(“GET”, goodsSurplusKey)if goodsSurplus == false then return 0end– 没有剩余可抢购物品goodsSurplus = tonumber(goodsSurplus)if goodsSurplus <= 0 then return 0endflag = redis.call(“SADD”, buyMembersKey, memberUid)flag = redis.call(“DECR”, goodsSurplusKey)return 1即使不了解 lua,相信你也可以将上面的脚本看个一二,其中–开始的是单行注释。local用来声明局部变量,redis lua 脚本中的所有变量都应该声明为local xxx,避免在持久化、复制的时候产生各种问题。KEYS和ARGV是两个全局变量,就像 PHP 中的$argc、$argv一样,脚本执行时传入的参数会写入这两个变量,供我们在脚本中使用。redis.call用来执行 redis 现有命令,传参跟 redis 命令行执行时传入参数顺序一致。另外 redis lua 脚本中用到 lua table 的地方还比较多,这里要注意,lua 脚本中的 table 下标是从 1 开始的,比如KEYS、ARGV,这里跟其他语言不一样,需要注意。对于主要使用 PHP 这种弱类型语言开发同学来说,一定要注意变量的类型,不同类型比较的时候可能会出现类似attempt to compare string with number的提示,这个时候使用 lua 的tonumber将字符串转换为数字在进行比较即可。比如我们使用GET去获取一个值,然后跟 0 比较大小,就需要将获取出来的字符串转换为数字。在调试之前呢,我们先看看效果,将上面的代码保存到 lua 文件中/path/to/buy.lua,然后运行redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984即可执行脚本,执行之后返回-1,因为我们未设置商品数量,set goodsSurplus 5之后再次执行,效果如下:➜ ~ redis-cli set goodsSurplus 5OK➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984(integer) 1➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984(integer) 0➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742983(integer) 1➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742982(integer) 1➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742981(integer) 1➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742980(integer) -1➜ ~ redis-cli –eval /path/to/buy.lua hadBuyUids goodsSurplus , 58247(integer) -1在命令行运行脚本的时候,脚本后面传入的是参数,通过 , 分隔为两组,前面是键,后面是值,这两组分别写入KEYS和ARGV。分隔符一定要看清楚了,逗号前后都有空格,漏掉空格会让脚本解析传入参数异常。debug 调试上一小节,我们写了很长一段 redis lua 脚本,怎么调试呢,有没有像 GDB 那样的调试工具呢,答案是肯定的。redis 从 v3.2.0 开始支持 lua debugger,可以加断点、print 变量信息、展示正在执行的代码……我们结合上一小节的脚本,来详细说说 redis 中 lua 脚本的调试。如何进入调试模式执行redis-cli –ldb –eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984,进入调试模式,比之前执行的时候多了参数–ldb,这个参数是开启 lua dubegger 的意思,这个模式下 redis 会 fork 一个进程进入隔离环境,不会影响 redis 正常提供服务,但调试期间,原始 redis 执行命令、脚本的结果也不会体现到 fork 之后的隔离环境之中。因此呢,还有另外一种调试模式–ldb-sync-mode,也就是前面提到的同步模式,这个模式下,会阻塞 redis 上所有的命令、脚本,直到脚本退出,完全模拟了正式环境使用时候的情况,使用的时候务必注意这点。调试命令详解这一小节的内容是调试时候的详细命令,可以粗略阅读后跳过,等使用的时候再回来查询帮助信息[h]elp调试模式下,输入h或者help展示调试模式下的全部可用指令。流程相关[s]tep 、 [n]ext 、 [c]continue执行当前行代码,并停留在下一行,如下所示* Stopped at 4, stop reason = step over-> 4 local buyMembersKey = tostring(KEYS[1])lua debugger> n* Stopped at 5, stop reason = step over-> 5 local memberUid = tonumber(ARGV[1])lua debugger> n* Stopped at 6, stop reason = step over-> 6 local goodsSurplusKey = tostring(KEYS[2])lua debugger> s* Stopped at 7, stop reason = step over-> 7 local hasBuy = redis.call(“sIsMember”, buyMembersKey, memberUid)continue从当前行开始执行代码直到结束或者碰到断点。展示相关[l]list 、 [l]list [line] 、 [l]list [line] [ctx] 、 [w]hole展示当前行附近的代码,[line]是重新指定中心行,[ctx]是指定展示中心行周围几行代码。[w]hole是展示所有行代码打印相关[p]rint 、 [p]rint <var>打印当前所有局部变量,<var>是打印指定变量,如下所示:lua debugger> print<value> goodsSurplus = nil<value> flag = nil<value> buyMembersKey = “hadBuyUids”<value> memberUid = 58247lua debugger> print buyMembersKey<value> “hadBuyUids"断点相关[b]reak 、 [b]reak <line> 、 [b]reak -<line> 、 [b]reak 0展示断点、像指定行添加断点、删除指定行的断点、删除所有断点其他命令[r]edis <cmd> 、 [m]axlen [len] 、 [a]bort 、 [e]eval <code> 、 [t]race在调试其中执行 redis 命令设置展示内容的最大长度,0表示不限制退出调试模式,同步模式下(设置了参数–ldb-sync-mode)修改会保留。执行一行 lua 代码。展示执行栈。详细说下[m]axlen [len]命令,如下代码:local myTable = {}local count = 0while count < 1000 do myTable[count] = count count = count + 1endreturn 1在最后一行打印断点,执行print可以看到,输出了一长串内容,我们执行maxlen 10之后,再次执行print可以看到打印的内容变少了,设置为maxlen 0之后,再次执行可以看到所有的内容全部展示了。详细说下[t]race命令,代码如下:local function func1(num) num = num + 1 return numendlocal function func2(num) num = func1(num) num = num + 1 return numendfunc2(123)执行b 2在 func1 中打断点,然后执行c,断点地方停顿,再次执行t,可以到如下信息:lua debugger> tIn func1:->#3 return numFrom func2: 7 num = func1(num)From top level: 12 func2(123)请求限流至此,算是对 redis lua 脚本有了基本的认识,基本语法、调试也做了了解,接下来就实现一个请求限流器。流程和代码如下:–[[ 传入参数: 业务标识 ip 限制时间 限制时间内的访问次数]]–local busIdentify = tostring(KEYS[1])local ip = tostring(KEYS[2])local expireSeconds = tonumber(ARGV[1])local limitTimes = tonumber(ARGV[2])local identify = busIdentify .. “” .. iplocal times = redis.call(“GET”, identify)–[[ 获取已经记录的时间 获取到继续判断是否超过限制 超过限制返回0 否则加1,返回1]]–if times ~= false then times = tonumber(times) if times >= limitTimes then return 0 else redis.call(“INCR”, identify) return 1 endend– 不存在的话,设置为1并设置过期时间local flag = redis.call(“SETEX”, identify, expireSeconds, 1)return 1将上面的 lua 脚本保存到/path/to/limit.lua,执行redis-cli –eval /path/to/limit.lua limit_vgroup 192.168.1.19 , 10 3,表示 limit_vgroup 这个业务,192.168.1.1 这个 ip 每 10 秒钟限制访问三次。好了,至此,一个请求限流功能就完成了,连续执行三次之后上面的程序会返回 0,过 10 秒钟在执行,又可以返回 1,这样便达到了限流的目的。有同学可能会说了,这个请求限流功能还有值得优化的地方,如果连续的两个计数周期,第一个周期的最后请求 3 次,接着马上到第二个周期了,又可以请求了,这个地方如何优化呢,我们接着往下看。请求限流优化上面的计数器法简单粗暴,但是存在临界点的问题。为了解决这个问题,引入类似滑动窗口的概念,让统计次数的周期是连续的,可以很好的解决临界点的问题,滑动窗口原理如下图所示:建立一个 redis list 结构,其长度等价于访问次数,每次请求时,判断 list 结构长度是否超过限制次数,未超过的话,直接加到队首返回成功,否则,判断队尾一条数据是否已经超过限制时间,未超过直接返回失败,超过删除队尾元素,将此次请求时间插入队首,返回成功。local busIdentify = tostring(KEYS[1])local ip = tostring(KEYS[2])local expireSeconds = tonumber(ARGV[1])local limitTimes = tonumber(ARGV[2])– 传入额外参数,请求时间戳local timestamp = tonumber(ARGV[3])local lastTimestamplocal identify = busIdentify .. “” .. iplocal times = redis.call(“LLEN”, identify)if times < limitTimes then redis.call(“RPUSH”, identify, timestamp) return 1endlastTimestamp = redis.call(“LRANGE”, identify, 0, 0)lastTimestamp = tonumber(lastTimestamp[1])if lastTimestamp + expireSeconds >= timestamp then return 0endredis.call(“LPOP”, identify)redis.call(“RPUSH”, identify, timestamp)return 1上面的 lua 脚本保存到/path/to/limit_fun.lua,执行redis-cli –eval /path/to/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999即可。最开始,我想着把时间戳计算redis.call(“TIME”)也放入 redis lua 脚本中,后来发现使用的时候 redis 会报错,这是因为 redis 默认情况复制 lua 脚本到备机和持久化中,如果脚本是一个非纯函数(pure function),备库中执行的时候或者宕机恢复的时候可能产生不一致的情况,这里可以类比 mysql 中基于 SQL 语句的复制模式。redis 在 3.2 版本中加入了redis.replicate_commands函数来解决这个问题,在脚本第一行执行这个函数,redis 会将修改数据的命令收集起来,然后用MULTI/EXEC包裹起来,这种方式称为script effects replication,这个类似于 mysql 中的基于行的复制模式,将非纯函数的值计算出来,用来持久化和主从复制。我们这里将变动参数提到调用方这里,调用者传入时间戳来解决这个问题。另外,redis 从版本 5 开始,默认支持script effects replication,不需要在第一行调用开启函数了。如果是耗时计算,这样当然很好,同步、恢复的时候只需要计算一次后边就不用计算了,但是如果是一个循环生成的数据,可能在同步的时候会浪费更多的带宽,没有脚本来的更直接,但这种情况应该比较少。至此,脚本优化完成了,但我又想到一个问题,我们的环境是单机环境,如果是分布式环境的话,脚本怎么执行、何处理呢,接下来一节,我们来讨论下这个问题。集群环境中 lua 处理redis 集群中,会将键分配的不同的槽位上,然后分配到对应的机器上,当操作的键为一个的时候,自然没问题,但如果操作的键为多个的时候,集群如何知道这个操作落到那个机器呢?比如简单的mget命令,mget test1 test2 test3,还有我们上面执行脚本时候传入多个参数,带着这个问题我们继续。首先用 docker 启动一个 redis 集群,docker pull grokzen/redis-cluster,拉取这个镜像,然后执行docker run -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 –name redis-cluster-script -e “IP=0.0.0.0” grokzen/redis-cluster启动这个容器,这个容器启动了一个 redis 集群,3 主 3 从。我们从任意一个节点进入集群,比如redis-cli -c -p 7003,进入后执行cluster nodes可以看到集群的信息,我们链接的是从库,执行set lua fun,有同学可能会问了,从库也可以执行写吗,没问题的,集群会计算出 lua 这个键属于哪个槽位,然后定向到对应的主库。执行mset lua fascinating redis powerful,可以看到集群反回了错误信息,告诉我们本次请求的键没有落到同一个槽位上(error) CROSSSLOT Keys in request don’t hash to the same slot同样,还是上面的 lua 脚本,我们加上集群端口号,执行redis-cli -p 7000 –eval /tmp/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999,一样返回上面的错误。针对这个问题,redis官方为我们提供了hash tag这个方法来解决,什么意思呢,我们取键中的一段来计算 hash,计算落入那个槽中,这样同一个功能不同的 key 就可以落入同一个槽位了,hash tag 是通过{}这对括号括起来的字符串,比如上面的,我们改为mset lua{yes} fascinating redis{yes} powerful,就可以执行成功了,我这里 mset 这个操作落到了 7002 端口的机器。同理,我们对传入脚本的键名做 hash tag 处理就可以了,这里要注意不仅传入键名要有相同的 hash tag,里面实际操作的 key 也要有相同的 hash tag,不然会报错Lua script attempted to access a non local key in a cluster node,什么意思呢,就拿我们上面的例子来说,执行的时候如下所示,可以看到 , 前面的两个键都加了 hash tag —— yes,这样没问题,因为脚本里面只是用了一个拼接的 key —— limit_vgroup{yes}_192.168.1.19{yes}。redis-cli -c -p 7000 –eval /tmp/limit_fun.lua limit_vgroup{yes} 192.168.1.19{yes} , 10 3 1548660999如果我们在脚本里面加上redis.call(“GET”, “yesyes”)(别让这个键跟我们拼接的键落在一个solt),可以看到就报了上面的错误,所以在执行脚本的时候,只要传入参数键、脚本里面执行 redis 命令时候的键有相同的 hash tag 即可。另外,这里有个 hash tag 规则:键中包含{字符;建中包含{字符,并在{字符右边;并且{,}之间有至少一个字符,之间的字符就用来做键的 hash tag。所以,键limit_vgroup{yes}_192.168.1.19{yes}的 hash tag 是 yes。foo{}{bar}键的 hash tag就是它本身。foo{{bar}}键的 hash tag 是 {bar。使用 golang 连接使用 redis这里我们使用 golang 实例展示下,通过ForEachMaster将 lua 脚本缓存到集群中的每个 node,并保存返回的 sha 值,以后通过 evalsha 去执行代码。package mainimport ( “github.com/go-redis/redis” “fmt”)func createScript() *redis.Script { script := redis.NewScript( local busIdentify = tostring(KEYS[1]) local ip = tostring(KEYS[2]) local expireSeconds = tonumber(ARGV[1]) local limitTimes = tonumber(ARGV[2]) -- 传入额外参数,请求时间戳 local timestamp = tonumber(ARGV[3]) local lastTimestamp local identify = busIdentify .. "_" .. ip local times = redis.call("LLEN", identify) if times &lt; limitTimes then redis.call("RPUSH", identify, timestamp) return 1 end lastTimestamp = redis.call("LRANGE", identify, 0, 0) lastTimestamp = tonumber(lastTimestamp[1]) if lastTimestamp + expireSeconds &gt;= timestamp then return 0 end redis.call("LPOP", identify) redis.call("RPUSH", identify, timestamp) return 1 ) return script}func scriptCacheToCluster(c *redis.ClusterClient) string { script := createScript() var ret string c.ForEachMaster(func(m *redis.Client) error { if result, err := script.Load(m).Result(); err != nil { panic(“缓存脚本到主节点失败”) } else { ret = result } return nil }) return ret}func main() { redisdb := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{ “:7000”, “:7001”, “:7002”, “:7003”, “:7004”, “:7005”, }, }) // 将脚本缓存到所有节点,执行一次拿到结果即可 sha := scriptCacheToCluster(redisdb) // 执行缓存脚本 ret := redisdb.EvalSha(sha, []string{ “limit_vgroup{yes}”, “192.168.1.19{yes}”, }, 10, 3,1548660999) if result, err := ret.Result(); err != nil { fmt.Println(“发生异常,返回值:”, err.Error()) } else { fmt.Println(“返回值:”, result) } // 示例错误情况,sha 值不存在 ret1 := redisdb.EvalSha(sha + “error”, []string{ “limit_vgroup{yes}”, “192.168.1.19{yes}”, }, 10, 3,1548660999) if result, err := ret1.Result(); err != nil { fmt.Println(“发生异常,返回值:”, err.Error()) } else { fmt.Println(“返回值:”, result) }}执行上面的代码,返回值如下:返回值: 0发生异常,返回值: NOSCRIPT No matching script. Please use EVAL.好了,目前为止,相信你对 redis lua 脚本已经有了很好的了解,可以实现一些自己想要的功能了,感谢大家的阅读。 ...

January 30, 2019 · 5 min · jiezi

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。weather = { dbname = “test.db”, tablename = “weather”, id = {“IntegerField”,{unique = true, null = false, primary_key = true}}, wind = {“TextField”,{}}, wind_direction = {“TextField”,{}}, sun_info = {“TextField”,{}}, low = {“IntegerField”,{}}, high = {“IntegerField”,{}}, city = {“TextField”,{}}, },定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步获取模型对象local Table = require(‘orm.class.table’)local _weatherTable = Table(“weather”)准备数据,建立数据对象local t = {}t.wind = flDict[v.fg]t.wind_direction = fxDict[v.ff]t.sun_info = v.fit.low = tonumber(v.fd)t.high = tonumber(v.fc)t.id = it.city = citylocal weather = _weatherTable(t)保存数据weather:save()读取数据_weatherTable.get:all():getPureData()是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。Flutter ORM数据库原理详解好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。1、save2、select where3、select PrimaryKey4、update where5、update PrimaryKey6、delete where7、delete PrimaryKey这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。下面详细介绍七种基本操作的逻辑。save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。结语目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter应用。 ...

January 21, 2019 · 1 min · jiezi

【RPA插件开发】使用 Lua 扩展 UiBot 的功能

前言:UiBot 除了自带的强大功能外,还允许有编程经验的开发人员对功能进行自由扩展,目前 UiBot 支持以下四种扩展方式:Python 插件、Lua 插件、Lua Mod 插件、COM 插件( .Net 也使用这种方式扩展)本期教程将为大家讲解如何利用这四种扩展方式扩充 UiBot 的功能。LUA 插件开发教程:插件开发快速上手打开 UiBot 安装目录下的 extend lua 目录,在这个目录下创建 luatest.lua 文件(记得修改文件扩展名),使用记事本打开这个文件,写入如下代码,然后将文件另存为无BOM(这点很重要)的 utf-8 编码格式,千万不要用Windows自带的记事本存(有BOM):function QMPlugin.add(n1, n2)return n1 + n2end插件保存后,打开 UiBot 在源代码视图写入代码:Import luatesttraceprint luatest.add(1, 1)运行 UiBot 脚本,运行后结果如下(输出2)代表插件调用正常:如果运行结果不正常,可根据输出窗格的报错提示尝试解决,如果报错为 模块test没有加载成功,则检查 test.py 中的代码是否正确、文件名和路径是否正确(例如文件后缀问题)开发环境部署(xywh Script)目前没有很好用的 LUA 免费开发环境,推荐使用“叶飞”(也就是我自己 -_- 开发的 xywh Script 进行开发和调试,但是为了适配 UiBot,我需要对 xywh Script 做一些更新,这段之后再放上来)UiBot 调用方法插件的文件名即为插件名,例如我们编写的插件文件名为 luatest.lua,则在 UiBot 中使用 Import luatest 即可载入插件。对于 Lua 插件来说,所有写在 QMPlugin 表内的函数都可以使用 UiBot 调用。LUA 插件与 UiBot 的结合度较好,支持可选、可变参数的传递。插件使用的其他组件安装到 UiBotLua插件如果使用了第三方的库,则必须将第三方的库添加到 UiBot 安装目录下的 lib lua 文件夹中。Lua 教程Lua 教程推荐 Programming in lua(中文译名:LUA程序设计),因为没有稳定的阅读渠道,需要自行寻找。另外 Lua 5.3 官方手册也是学习 Lua 时非常重要的文档:http://cloudwu.github.io/lua5…现在下载还有机会参与到我们的活动当中!还等什么?赶快来体验!活动期间(1月16日至2月5日)下载注册登录UiBot不仅有机会可以和产品大牛、技术大神亲密接触,还有更多高级培训课程、个人版年卡等丰厚奖品等你来拿。我们每一次努力只为让用户获得更好的体验! ...

January 17, 2019 · 1 min · jiezi

k8s与log--利用lua为fluent bit添加一个filter

前言之前我们介绍过fluent bit这个日志收集神器。最近我们遇到奇葩的需求,不得不利用lua编写fluent bit的filter,来满足需求。首先介绍一下需求:非容器的日志团队使用filebeat, 其配置文件部分如下:processors:- dissect: tokenizer: “/data/logs/%{appname}/%{filename}.log” field: “source” target_prefix: ““即需要从日志record的source filed 提取appname和filename两个filed。fluent bit 并没有如此的插件,所以不得不自己实现。实现lua编写filter规范官方给出的示例如下:function cb_print(tag, timestamp, record) return code, timestamp, recordendFunction 输入参数Function ArgumentsnamedescriptiontagName of the tag associated with the incoming record.timestampUnix timestamp with nanoseconds associated with the incoming record. The original format is a double (seconds.nanoseconds)recordLua table with the record contentReturn ValuesEach callback must return three values:namedata typedescriptioncodeintegerThe code return value represents the result and further action that may follows. If code equals -1, means that filter_lua must drop the record. If code equals 0 the record will not be modified, otherwise if code equals 1, means the original timestamp or record have been modified so it must be replaced by the returned values from timestamp (second return value) and record (third return value).timestampdoubleIf code equals 1, the original record timestamp will be replaced with this new value.recordtableif code equals 1, the original record information will be replaced with this new value. Note that the format of this value must be a valid Lua table.理解上面的规范可以结合下面的写法。注意返回值的code。代码实现编写实现类似功能的lua文件,如下:function dissect(tag, timestamp, record) source = record[“source”] if (source == nil) then return 0, 0, 0 else new_record = record local result = { } local from = 1 local delim_from, delim_to = string.find( source, “/”, from ) while delim_from do table.insert( result, string.sub( source, from , delim_from-1 ) ) from = delim_to + 1 delim_from, delim_to = string.find( source, “/”, from ) end table.insert( result, string.sub( source, from ) ) new_record[“appname”] = result[7] new_record[“filename”] = string.sub( result[8], 1, -5 ) return 1, timestamp, new_record end end备注:在我们k8s环境下,业务日志挂盘路径类似于下面的格式:source = /data/logs/default/tomcat/742473c7-17dc-11e9-afc5-0a07a5c4fbe2/appname/filename.logresult[7]之所以出现这种及其容易出现bug的写法,一是由于我们这边有严格的日志规范,另外,只是给大家提供一种lua写filter的思路。制作镜像我们是基于fluent bit 1.0.2 。所以找到官方的代码仓库,git clone 下来,稍作更改。新的dockerfile如下:FROM debian:stretch as builder# Fluent Bit versionENV FLB_MAJOR 1ENV FLB_MINOR 0ENV FLB_PATCH 2ENV FLB_VERSION 1.0.2ENV DEBIAN_FRONTEND noninteractiveENV FLB_TARBALL http://github.com/fluent/fluent-bit/archive/v$FLB_VERSION.zipRUN mkdir -p /fluent-bit/bin /fluent-bit/etc /fluent-bit/log /tmp/fluent-bit-master/RUN apt-get update && \ apt-get install -y –no-install-recommends \ build-essential \ cmake \ make \ wget \ unzip \ libssl1.0-dev \ libasl-dev \ libsasl2-dev \ pkg-config \ libsystemd-dev \ zlib1g-dev \ ca-certificates \ && wget -O “/tmp/fluent-bit-${FLB_VERSION}.zip” ${FLB_TARBALL} \ && cd /tmp && unzip “fluent-bit-$FLB_VERSION.zip” \ && cd “fluent-bit-$FLB_VERSION”/build/ \ && rm -rf /tmp/fluent-bit-$FLB_VERSION/build/WORKDIR /tmp/fluent-bit-$FLB_VERSION/build/RUN cmake -DFLB_DEBUG=On \ -DFLB_TRACE=Off \ -DFLB_JEMALLOC=On \ -DFLB_TLS=On \ -DFLB_SHARED_LIB=Off \ -DFLB_EXAMPLES=Off \ -DFLB_HTTP_SERVER=On \ -DFLB_IN_SYSTEMD=On \ -DFLB_OUT_KAFKA=On ..RUN make -j $(getconf _NPROCESSORS_ONLN)RUN install bin/fluent-bit /fluent-bit/bin/# Configuration filesCOPY fluent-bit.conf \ parsers.conf \ parsers_java.conf \ parsers_extra.conf \ parsers_openstack.conf \ parsers_cinder.conf \ plugins.conf \ /fluent-bit/etc/COPY dissect.lua /fluent-bit/bin/FROM gcr.io/distroless/ccMAINTAINER Eduardo Silva <eduardo@treasure-data.com>LABEL Description=“Fluent Bit docker image” Vendor=“Fluent Organization” Version=“1.1"COPY –from=builder /usr/lib/x86_64-linux-gnu/sasl /usr/lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libz /usr/lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libz* /lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libssl.so* /usr/lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libcrypto.so* /usr/lib/x86_64-linux-gnu/# These below are all needed for systemdCOPY –from=builder /lib/x86_64-linux-gnu/libsystemd* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libselinux.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/liblzma.so* /lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/liblz4.so* /usr/lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libgcrypt.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libpcre.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libgpg-error.so* /lib/x86_64-linux-gnu/COPY –from=builder /fluent-bit /fluent-bit#EXPOSE 2020# Entry pointCMD ["/fluent-bit/bin/fluent-bit”, “-c”, “/fluent-bit/etc/fluent-bit.conf”]注意增加了 COPY dissect.lua /fluent-bit/bin/ 。然后就build镜像即可。使用姿势使用比较简单的。demo如下: [FILTER] Name lua Match app.* script /fluent-bit/bin/dissect.lua call dissectscript lua脚本的存放路径。call 即为lua函数名。总结通过写这个filter,有一下几个感悟吧。官方的镜像基于谷歌的distroless镜像,没有shell,没有包管理,调试起来很费力。平时的业务容器化场景中,明显的不合适,与阿里的富容器思维南辕北辙。当然除非你公司的业务开发能力足够强。fluent bit 相关的资料还是有点少。遇到问题和使用一些不明白的地方,解决起来费力。除非你是c专家。官方文档写的也不够详细,只是描述了个大概。 ...

January 14, 2019 · 3 min · jiezi

Flutter通用基础库flutter_luakit_plugin

使用flutter_luakit_plugin作为基础库开发flutter应用文章开头我们先开门见山给出使用flutter_luakit_plugin作为基础库开发和普通flutter的区别。由于flutter定位是便携UI包,flutter提供的基础库功能是不足以满足复杂数据的app应用的,一般flutter开发模式如下图所示,当flutter满足不了我们的需求的时候,使用methodchannel和eventchannel调用native接口。而使用flutter_luakit_plugin作为基础库的开发模式如下图所示,用lua来写逻辑层代码,用flutter写UI代码。luakit 提供了丰富的功能支持,可以支持大部分app的逻辑层开发,包括数据库orm,线程管理,http请求,异步socket,定时器,通知,json等等。用户只需要写dart代码和lua代码,不需要写oc、swift或java、kotlin代码,从而大幅提升代码的一致性(所有运行代码都是跨平台的)。flutter_luakit_plugin由来Flutter诞生的时候我很兴奋,因为我对跨平台开发UI的看法一直是不看好的,最主要的原因是无法获得体验一致性,但是Flutter前无古人的解决了这个问题,真正做到一端开发的UI,无论多复杂,在另一端是可以得到一致的体验的,做不到这点的跨平台UI方案实际上并没有达到跨平台节省工作量的效果,Flutter做到了。Flutter1.0.0 发布了,我认为移动端跨平台开发所需要所有元素都已经齐备了,我们尝试使用Flutter做一些功能,一个版本之后我们总结了一些问题。Flutter是一套UI解决方案,但一个功能除了UI,还需要很多支持,网络请求,长连接,短连接,数据库,线程控制等等,这些方面Flutter生态中提供得比较差,没有ios 或者android那么多成熟的解决方案。Flutter 为了克服这问题,提供了一个解决方案,利用methodchannel和eventchannel调用ios和android的接口,利用原生成熟的方案做底层逻辑支撑。我们一开始也是这样解决,但后续的麻烦也来了,由于methodchannel和eventchannel实现的方法是不跨平台的,Flutter从ios和android得到的数据的格式,事件调用的时机等,两个平台的实现是不一样的,基本不可能完全统一,可以这样说,一个功能在一个端能跑通,在另一个端第一次跑一定跑不通,然后就要花大量的时间进行调试,适配,这样做之后跨平台的优势荡然无存,大家就会不断扯皮。相信我,下面的对话会成为你们的日常。ios开发:“你们android写的界面ios跑不起来”Android 开发:“我们android能跑啊,iOS接口写得不对吧”ios开发:“哪里不对,android写的界面,android帮忙调吧”Android 开发:“我又不是ios开发,我怎么调”当一个已有的app要接入flutter,必然会产生一种情况,就是flutter体系里面的数据和逻辑,跟外部原生app的逻辑是不通的,简单说明一下,就是flutter写的业务逻辑通常是用dart语言写的,我们在原生用object-c、swift或者java、kotlin写的代码是不可以脱离flutter的界面调用dart写的逻辑的,这种互通性的缺失,会导致很多数据联动做不到,譬如原生界面要现实一个flutter页面存下来的数据,或者原生界面要为flutter页面做一些预加载,这些都很不方便,主要是下图中,当flutter界面没调用时,从原生调用flutter接口是不允许的。之前我曾经开源一个纯逻辑层的跨平台解决方案luakit(附上luakit的起源),里面提供一个业务开发所需要的基本能力,包括网络请求,长连接,短连接,orm数据库,线程,通知机制等等,而且这些能力都是稳定的、跨平台而且经过实际业务验证过的方案。做完一个版本纯flutter之后,我意识到可以用一种新的开发模式来进行flutter开发,这样可以避免我上面提到的两个问题,我们团队马上付诸实施,做了另一个版本的flutter+luakit的尝试,即用flutter做界面,用lua来写逻辑,结构图如下。新的方案开发效率得到极大的提升,不客气的说真正实现了跨平台,一个业务,从页面到逻辑,所有的代码一气呵成全部由脚本完成(dart+lua),完全不用object-c、swift或者java、kotlin来写逻辑,这样一个业务基本就可以无缝地从一端直接搬到另一端使用,所以我写了这篇文章来介绍我们团队的这个尝试,也把我们的成果flutter_luakit_plugin开源了出来,让这种开发模式帮助到更多flutter开发团队。细说开发模式下一步我们一起看看如何用flutter配合lua实现全部代码都是跨平台的。我们提供了一个 demo project,供大家参考。dart写界面在demo中所有的ui都写在了main.dart,当然在真实业务中肯定复杂很多,但是并不影响我们的开发模式。dart调用lua逻辑接口FlutterLuakitPlugin.callLuaFun(“WeatherManager”, “getWeather”).then((dynamic d) { print(“getWeather” + d.toString()); setState(() { weathers = d; });});上面这段代码的意思是调用WeatherManager的lua模块,里面提供的getWeather方法,然后把得到的数据以future的形式返回给dart,上面的代码相当于调用下面一段lua代码require(‘WeatherManager’).getWeather( function (d) end)然后剩下的事情就到lua,在lua里面可以使用luakit提供的所有强大功能,一个app所需要的绝大部分的功能应该都提供了,而且我们还会不断扩展。大家可能会担心dart和lua的数据格式转换问题,这个不用担心,所有细节在flutter_luakit_plugin都已经做好封装,使用者尽管像使用dart接口那样去使用lua接口即可。在lua中实现所有的非UI逻辑这个demo(WeatherManager.lua)已经演示了如何使用luakit的相关功能,包括,网络,orm数据库,多线程,数据解析,等等如果实在有flutter_luakit_plugin没有支持的功能,可以走回flutter提供的methodchannel和eventchannel的方式实现如何接入flutter_luakit_plugin经过了几个月磨合实践,我们团队已经把接入flutter_luakit_plugin的成本降到最低,可以说是非常方便接入了。我们已经把flutter_luakit_plugin发布到flutter官方的插件仓库。首先,要像其他flutter插件一样,在pubspec.yaml里面加上依赖,可参考demo配置flutter_luakit_plugin: ^1.0.0然后在ios项目的podfile加上ios的依赖,可参考demo配置source ‘https://github.com/williamwen1986/LuakitPod.git'source ‘https://github.com/williamwen1986/curl.git'pod ‘curl’, ‘> 1.0.0’pod ‘LuakitPod’, ‘> 1.0.13’然后在android项目app的build.gradle文件加上android的依赖,可参考demo配置repositories { maven { url “https://jitpack.io” }}dependencies { implementation ‘com.github.williamwen1986:LuakitJitpack:1.0.6’}最后,在需要使用的地方加上import就可以使用lua脚本了import ‘package:flutter_luakit_plugin/flutter_luakit_plugin.dart’;lua脚本我们默认的执行根路径在android是 assets/lua,ios默认的执行根路径是Bundle路径。flutter_luakit_plugin开发环境IDE–AndroidStudioflutter 官方推荐的IDE是androidstudio和visual studio code。我们在开发中觉得androidstudio更好用,所有我们同步也开发了luakit的androidstudio插件,名字就叫luakit。luakit插件提供了以下的一些功能。远程lua调试查找函数使用跳到函数定义跳到文件参数名字提示代码自动补全代码格式化代码语法检查标准lua api自动补全luakit api自动补全大部分功能,跟其他IDE没太多差别,这里我就不细讲了,我重点讲一下远程lua调试功能,因为这个跟平时调试ios和android设备有点不一样,下面我们详细介绍androidstudio luakit插件的使用。androidstudio安装luakit插件AndroidStudio->Preference..->Plugins->Browse reprositories…搜索Luakit并安装Luakit插件然后重启androidstudio配置lua项目打开 Project Struture 窗口选择 Modules、 Mark as Sources添加调试器选择 Edit Configurations …Select plus添加Lua Remote(Mobdebug)远程lua调试在开始调试lua之前,我们要在需要调试的lua文件加上下面一句lua代码。然后设上断点,即可调试。lua代码里面有两个参数,第一个是你调试用的电脑的ip地址,第二个是调试端口,默认是8172。require(“mobdebug”).start(“172.25.129.165”, 8172)luakit的调试是通过socket来传递调试信息的,所有调试机器务必我电脑保持在同一网段,有时候可能做不到,这里我们给出一下办法解决,我们日常调试也是这样解决的。首先让你的手机开热点,然后你的电脑连上手机的热点,现在就可以保证你的手机和电脑是同一网段了,然后查看电脑的ip地址,填到lua代码上,就可以实现调试了。flutter_luakit_plugin提供的api介绍(1) 数据库orm操作这是flutter_luakit_plugin里面提供的一个强大的功能,也是flutter现在最缺的,简单高效的数据库操作,flutter_luakit_plugin提供的数据库orm功能有以下特征面向对象自动创建和更新表结构自带内部对象缓存定时自动transaction线程安全,完全不用考虑线程问题具体可参考demo lua,下面只做简单介绍。定义数据模型– Add the define table to dbData.lua– Luakit provide 7 colum types– IntegerField to sqlite integer – RealField to sqlite real – BlobField to sqlite blob – CharField to sqlite varchar – TextField to sqlite text – BooleandField to sqlite bool– DateTimeField to sqlite integeruser = { dbname = “test.db”, tablename = “user”, username = {“CharField”,{max_length = 100, unique = true, primary_key = true}}, password = {“CharField”,{max_length = 50, unique = true}}, age = {“IntegerField”,{null = true}}, job = {“CharField”,{max_length = 50, null = true}}, des = {“TextField”,{null = true}}, time_create = {“DateTimeField”,{null = true}} },– when you use, you can do just like belowlocal Table = require(‘orm.class.table’)local userTable = Table(“user”)插入数据local userTable = Table(“user”)local user = userTable({ username = “user1”, password = “abc”, time_create = os.time()})user:save()更新数据local userTable = Table(“user”)local user = userTable.get:primaryKey({“user1”}):first()user.password = “efg"user.time_create = os.time()user:save()删除数据local userTable = Table(“user”)local user = userTable.get:primaryKey({“user1”}):first()user:delete()批量更新数据local userTable = Table(“user”)userTable.get:where({age__gt = 40}):update({age = 45})批量删除数据local userTable = Table(“user”)userTable.get:where({age__gt = 40}):delete()select数据local userTable = Table(“user”)local users = userTable.get:all()print(“select all ———–")local user = userTable.get:first()print(“select first ———–")users = userTable.get:limit(3):offset(2):all()print(“select limit offset ———–")users = userTable.get:order_by({desc(‘age’), asc(‘username’)}):all()print(“select order_by ———–")users = userTable.get:where({ age__lt = 30, age__lte = 30, age__gt = 10, age__gte = 10, username__in = {“first”, “second”, “creator”}, password__notin = {“testpasswd”, “new”, “hello”}, username__null = false }):all()print(“select where ———–")users = userTable.get:where({“scrt_tw”,30},“password = ? AND age < ?”):all()print(“select where customs ———–")users = userTable.get:primaryKey({“first”,“randomusername”}):all()print(“select primaryKey ———–")联表操作local userTable = Table(“user”)local newsTable = Table(“news”)local user_group = newsTable.get:join(userTable):all()print(“join foreign_key”)user_group = newsTable.get:join(userTable,“news.create_user_id = user.username AND user.age < ?”, {20}):all()print(“join where “)user_group = newsTable.get:join(userTable,nil,nil,nil,{create_user_id = “username”, title = “username”}):all()print(“join matchColumns “)(2) 通知机制通知机制提供了一个低耦合的事件互通方法,即在原生或者lua或者dart注册消息,在任何地方抛出的消息都可以接收到。Flutter 添加监听消息void notify(dynamic d) {}FlutterLuakitPlugin.addLuaObserver(3, notify);Flutter 取消监听FlutterLuakitPlugin.removeLuaObserver(3, notify);Flutter抛消息FlutterLuakitPlugin.postNotification(3, data);lua 添加监听消息demo codelocal listenerlua_notification.createListener(function (l) listener = l listener:AddObserver(3, function (data) print(“lua Observer”) if data then for k,v in pairs(data) do print(“lua Observer”..k..v) end end end )end);lua抛消息demo codelua_notification.postNotification(3,{ lua1 = “lua123”, lua2 = “lua234”})ios 添加监听消息demo code_notification_observer.reset(new NotificationProxyObserver(self));_notification_observer->AddObserver(3);- (void)onNotification:(int)type data:(id)data{ NSLog(@“object-c onNotification type = %d data = %@”, type , data);}ios抛消息demo codepost_notification(3, @{@“row”:@(2)});android 添加监听消息demo codeLuaNotificationListener listener = new LuaNotificationListener();INotificationObserver observer = new INotificationObserver() { @Override public void onObserve(int type, Object info) { HashMap<String, Integer> map = (HashMap<String, Integer>)info; for (Map.Entry<String, Integer> entry : map.entrySet()) { Log.i(“business”, “android onObserve”); Log.i(“business”, entry.getKey()); Log.i(“business”,”"+entry.getValue()); } }};listener.addObserver(3, observer);android抛消息demo codeHashMap<String, Integer> map = new HashMap<String, Integer>();map.put(“row”, new Integer(2));NotificationHelper.postNotification(3, map);(3) http requestflutter本身提供了http请求库dio,不过当项目的逻辑接口想在flutter,原生native都可用的情况下,flutter写的逻辑代码就不太合适了,原因上文已经提到,原生native是不可以随意调用flutter代码的,所以遇到这种情况,只有luakit合适,lua写的逻辑接口可以在所有地方调用,flutter 、ios、android都可以方便的使用lua代码,下面给出luakit提供的http接口,demo code。– url , the request url– isPost, boolean value represent post or get– uploadContent, string value represent the post data– uploadPath, string value represent the file path to post– downloadPath, string value to tell where to save the response– headers, tables to tell the http header– socketWatcherTimeout, int value represent the socketTimeout– onResponse, function value represent the response callback– onProgress, function value represent the onProgress callbacklua_http.request({ url = “http://tj.nineton.cn/Heart/index/all?city=CHSH000000", onResponse = function (response) end})(4) Async socket异步socket长连接功能也是很多app开发所依赖的,flutter只支持websocket协议,如果app想使用基础的socket协议,那就要使用flutter_luakit_plugin提供的socket功能了,使用也非常简单,demo code,在callback里面拿到数据后可以使用上文提到的通知机制把数据传回到flutter层。local socket = lua_asyncSocket.create(“127.0.0.1”,4001)socket.connectCallback = function (rv) if rv >= 0 then print(“Connected”) socket:read() endend socket.readCallback = function (str) print(str) timer = lua_timer.createTimer(0) timer:start(2000,function () socket:write(str) end) socket:read()endsocket.writeCallback = function (rv) print(“write” .. rv)endsocket:connect()(5) json 解析json是最常用数据类型,使用可参考demolocal t = cjson.decode(responseStr)responseStr = cjson.encode(t)(6) 定时器timer定时器也是项目开发中经常用到的一个功能,定时器我们在orm框架的lua源码里面有用到,demolocal _timer_timer = lua_timer.createTimer(1)//0代表单次,1代表重复_timer:start(2000,function () end)_timer:stop()(7) 还有所有普通适合lua用的库都可以在flutter_luakit_plugin使用flutter技术积累相关链接flutter通用基础库flutter_luakit_pluginflutter_luakit_plugin使用例子《手把手教你编译Flutter engine》《手把手教你解决 Flutter engine 内存漏》修复内存泄漏后的flutter engine(可直接使用)修复内存泄漏后的flutter engine使用例子持续更新中… ...

January 9, 2019 · 3 min · jiezi

写后台总结

多对多关系的表,如果删除某个表的行,另外一个表怎么处理,关系表怎么处理比如模板表和模块表,还有一个中间关系表。因为模板引用模块,他们之间的引用关系在中间关系表里有数据表征,如果删除一个模板,中间表不用删除,因为查找模板所对应模块的时候,首先要到模板表里查找是否有该模板,而且中间表没有status字段表达是否被删除,它只是一个表达一个关系而已。因为模块被模板引用,当删除模块时,要先判断是否有被引用(在中间表里查找即可),如果有被引用,则不能删除。关系表永远只有被动处理为什么后台对数据结构和算法要求很高,我现在是了解了当用sql语句从数据库里拿到数据,需要解析成前端所需要的各种格式的数据,这时候各种数组,对象的操作,遍历等,怎么算节省效率,这时候就需要很强的数据操作能力了。关于数据库设计使用逻辑外键,而非物理外键设计表,刚开始不用想太多,想到怎样就设计成怎样好了,后面具体到细节的时候再修改。数据库范式1NF: 字段是最小的的单元不可再分2NF:满足1NF,表中的字段必须完全依赖于全部主键而非部分主键3NF:满足2NF,非主键外的所有字段必须互不依赖4NF:满足3NF,消除表中的多值依赖查询的时候不要使用select * ,一是影响查询速度,二是如果数据库字段改了,前台的变量名也要跟着改了视图,从表中到出的一个子集,可以限制每个用户可看到的数据库的区域数据库设计过程先设计实体关系图(erd)设计表结构应避免过多表的连接查询,连接查询中表越多,查询的执行速度越慢对于有父子关系的表,修改某一条数据,也要修改其下一级的数据,从而形成一个递归修改。方法1: mysql 只能用函数来实现递归delimiter // CREATE FUNCTION getParList(rootId INT)RETURNS varchar(1000) BEGIN DECLARE sTemp VARCHAR(1000); DECLARE sTempPar VARCHAR(1000); SET sTemp = ‘’; SET sTempPar =rootId; #循环递归 WHILE sTempPar is not null DO #判断是否是第一个,不加的话第一个会为空 IF sTemp != ’’ THEN SET sTemp = concat(sTemp,’,’,sTempPar); ELSE SET sTemp = sTempPar; END IF; SET sTemp = concat(sTemp,’,’,sTempPar); SELECT group_concat(pid) INTO sTempPar FROM treenodes where pid<>id and FIND_IN_SET(id,sTempPar)>0; END WHILE; RETURN sTemp; END方法2: ++在表中多加一个字段存所有父级的id,每次insert的时候将父级id拼起来存到表里, 后面查找的时候用 like 语句匹配,显然这种方法效率更高++规范化不是关系型数据库设计最重要的目标。对于第一二三范式,不需要严格遵守。有时候规范化粒度过细所导致的问题不比其所解决的问题少。现在关系型数据库模型不会扩充到3nf上,有时连3nf也不用,原因在于生成的表太多,所得的sql代码连接很复杂导致数据库响应时间过长,而且磁盘空间并不贵。4nf,5nf,dknf等趋向于在表中加入一些业务逻辑,这一点并无必要。应该由程序来处理业务逻辑,数据库只存数据。软删除和唯一约束的冲突软删除把state改为1之后,记录还存在。如果某一个字段是唯一约束的话,比如name,那么新增一个被删除掉一样name的记录,就会报唯一键约束的错误。MysqlMysql 比较灵活,既可以嵌入到应用程序中,也可以支持数据仓库,内容索引和部署软件,高可用的冗余系统,在线事务处理(OLTP)写锁(write lock)会减少并发,但是又是不可避免的。所以要让锁定对象更有选择性,尽量只锁定需要修改的部分数据。锁定的数据越少,系统的并发程度越高。锁策略就是控制锁的粒度。Mysql提供表锁(table lock),行锁(row lock)死锁,指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,并导致恶性循环start transaction;update stock_price set close = 10 where id=1update stock_price set close = 12 where id=2commit;start transaction;update stock_price set high = 100 where id=2update stock_price set high = 120 where id=1commit;如果两个事务都执行了第一条update语句,同时锁定了该行数据,接着每个事务都尝试去执行第二条update语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。 目前InnoDB处理死锁的方式是,将持有最少行级排他锁的事务进行回滚。数据库优化避免多表查询返回所有列,如下,查询的列越多,时间越长select * from t_agentleft join t_admin on t_agent.agent_id=t_admin.agent_id_fkleft join t_agent_ex on t_agent_agent_id=t_admin.agent_id_fk避免查询重复的数据,比如经常需要查询的数据可以放到缓存里对于查询需要扫描大量数据(比如列表查询),使用索引覆盖扫描,把用于where查询条件的列放到索引里。常用的查询条件列比如 agent_name 可以放到索引里。因为单查 agent_name 的情况比较多。select * from t_agent where agent_name=‘sam’是否将一个复杂查询转换成多个简单查询。在传统的实现中,强调在数据库层尽可能完成多的工作,这样做的逻辑是以前认为网络通信的代价很高,但是现在的网络越来越快,这方面的代价很小。当然,分开查询也是有代价的,这个问题需要在实际问题中衡量。删除大量数据时,可以一次删除一部分,比如一万行数据,分多次删除。复杂的多表查询,可以分开查,然后在后台代码里组装数据(当然用多表查询,可以简化代码)。4GL语言1GL 二进制语言2GL 汇编语言 二进制语言的文本缩写3GL 高级编程语言 c js java等4GL sql 两个特征 非过程性的;面向表的 ...

January 6, 2019 · 1 min · jiezi

百度云曲显平:AIOps时代下如何用运维数据系统性地解决运维问题?

本文是根据百度云智能运维负责人曲显平10月20日在msup携手魅族、Flyme、百度云主办的第十三期魅族技术开放日《百度云智能运维实践》演讲中的分享内容整理而成。内容简介:本文主要从百度运维技术的发展历程、如何做智能运维、故障管理场景、服务咨询场景和面对的挑战等几个方面介绍了百度云智能运维实践。百度运维技术的三个阶段第一阶段:基础运维平台 2008年2012年2008年,在百度运维部建立之前,还没有一个标准而统一的运维平台。例如,搜索、广告、贴吧都有各自的运维平台。存在的问题:技术和平台能力无法复用,业务之间需要交互时比较复杂。解决方法:①为帮助业务解决问题,我们把各个分散在不同业务的运维平台整合起来做成一套标准化运维平台;②有了统一运维平台后,运维部门内的角色就分为了两个,即标准的运维工程师和运维平台研发工程师。第二阶段:开放的运维平台 2012年2014年第一阶段仍然存在的问题: ①个性化需求很多,统一平台很难全部解决②PaaS出现之后,运维平台和PaaS的关系解决方法:①开放运维平台,即全部API化。②通过提供标准化的监控数据的采集、计算、报警能力,最基础的程序分发、数据分发、任务调度能力,解决自身平台的需求。③利用PaaS方法,把一些研发的技术平台和运维技术平台整合在一起,解决重复造轮子的问题。第三阶段:AIOps阶段 2014年开始百度从2014年就开始了智能运维的实践。最早的时候,我们更多是通过完善底层的大数据平台能力,提供一些数据分析和挖掘的算法和工具,解决运维数据没有得到合理运用,运维人工效率低等问题,这是偏大数据的方法。百度对于AIOps的理解在2015年,AI变得异常火热,百度也是想将自身先进的机器学习算法应用到运维领域之中,于是我们和百度的大数据实验室、深度学习实验室进行了合作。运维研究人员把需求和归整好的数据提交给实验室的人员,然后他们会根据数据训练模型,最终提供一些库和方法供业务使用。2016年,Gartner提出了AIOps这个词,也就是我们说的智能运维,这和百度的实践是不谋而合的。三个核心内容随着智能运维的发展,百度也是把数据、工程和策略三个,作为最核心内容来系统地解决运维行业的应用。从数据角度来讲,首先要构建一个完整的数据仓库,接着要建设运维知识库。知识库是在数据仓库上抽象进行的。从工程角度,一方面,分析数据和训练算法模型需要大数据平台和框架,另一方面,运维业务研发人员还做了一套运维工程研发框架,用以解决标准化、可扩展和复用的问题。这个框架十月份刚刚开源,感兴趣的朋友可以看下。在百度内部,一致的运维“语言”非常关键。我们要统一不同的工具和平台,形成一致的运维模式。所以不管是故障感知、故障诊断决策、弹性伸缩决策还是运维操作和执行,只有统一起来才能解决这个问题。一致不仅是数据一致、工程一致,还需要策略本身的一致性。自动驾驶分级在构建整个百度智能运维体系的过程中,我们重点参考了自动驾驶里的分级理论。百度是有这样两个部门的,一个叫L3,一个叫L4。L3部门重点在做类似于辅助驾驶或者高度辅助驾驶;L4部门做的是高度完全自动驾驶。下图是关于自动驾驶的分级。运维能力分级自动化运维能力分级当时我们团队参照这个自动驾驶分级,构建出了一个自动化运维能力的分级标准,用以评估我们各个方向的自动化水平,一共分为六个能力等级,即人工、工具辅助、部分自动化、有条件的自动化、高速自动化和完全自动化。关键点:决策规划由运维系统做出,而不是人人负责:制定优化目标(比如,可用性、效率、成本等)运维系统负责:根据其对待处理的需求、待解决的问题的理解,以及对运维对象的认知(经验),自主做出解决方案(规划)并在控制执行过程中根据目标和运维对象的状态反馈来适时调整执行规划。智能化运维能力分级在自动化能力分级之中,我们还细化出了一个智能化运维能力分级(我们始终认为智能运维是实现完全自动化运维的一种手段)。实现智能化能力,重点解决的是在运维感知和决策过程中,人工效率低和准确率不足的问题。关键点:决策规划由运维系统做出,而不是人人负责:制定优化目标(比如,可用性、效率、成本等)运维系统负责:根据其对待处理的需求、待解决的问题的理解,以及对运维对象的认知(经验),自主做出解决方案(规划)并在控制执行过程中根据目标和运维对象的状态反馈来适时调整执行规划。如何做运维我们希望每一个运维工具都像一个小型的运维机器人一样,解决运维的问题。运维工程师需要把每一个运维工具抽象化,同时也要像一个标准框架一样,可以在代码库里克隆,把框架代码复制下来。通过三个基本核心,感知、决策和执行来进行编写执行器,接着可以通过配置实现一些具体任务调度的配置或者并发执行的配置;每一个运维工程师要实现感知逻辑、决策逻辑、执行逻辑,利用运维核心解决可靠性的问题。在测试方面,要在线下建立看代码的逻辑去验证。结合这个看代码,把比较核心的运维故障抽象出来,再把一些常见的故障模拟出来,具体的情况可以在这里面运行;写完一个运维工具或者算法,需要直接在上面运行,从而检测出是否有效。故障处理场景百度内部如何解决故障处理场景故障处理场景一般分四个主要阶段:故障发现、服务止损、服务恢复、故障总结。在服务止损方面,核心是如何让用户感知不到这个故障,对于运维来讲,更多用的方法是隔离、降级,而非从代码BUG入手解决的问题。在服务恢复方面,这个一般是在服务止损或者说故障被隔离之后,很大程度上需要运维和研发共同合作,比如定位代码的BUG,最终要决定如何把线上的问题真正解决掉。恢复,更多用的是修复来解决。在百度,大多数的故障都是可以用隔离和降级解决的,只有那些极特殊的case,才会通过程序回滚来恢复。回滚风险很大,而且效率很低。在整个解决故障处理场景的阶段,每一个阶段都可以结合智能运维的方法。从开始服务部署、监控添加、故障发现、止损决策、止损操作、根因诊断、恢复操作,最后报告自动生成。把AIOps应用到故障处理最核心的基础是,全面覆盖监控。在百度,做的最全面的是云上的监控,所以包含这四个维度的监控:系统监控、业务监控、内网监控和外网监控。系统监控主要的监控对象是机器/容器和服务的动态内容;业务监控针对业务和用户的访问日志等;内网监控则针对IDC内网设备和内网链路;外网监控为了保障用户、运营商链路到百度IDC中间的状态。有了全面的监控之后,才能开始现在业界常提到的一个智能运维技术,自动异常检测。典型的异常检测场景有关异常检测场景,我为大家举三个典型的例子,第一个,周期波动的数据。上图中的蓝、绿、黄三条线分别代表着今天、昨天、上周的时间线,蓝线比较明显,后面还有绿线和黄线。它们相对来说周期性体现得特别强。这种数据很难用传统的计算方法设置阈值。针对这种场景,我们会使用不同类的算法,专门解决这种问题。第二个,关心突变的数据。突变的数据也是一个比较典型的场景,周期性数据更多参考的是天级和周级的数据,而这个场景更多说的是某一个细节层面,可以理解为它是对一小块数据的放大。第三个,关心是否超出了一定波动范围的数据。这种场景是我们用普通的监控方法很难覆盖的,很多情况下,其均值或基线不会有特别明显的变化,但系统现在确实出现了很大的不同状态,可能仅仅是波动更剧烈了,对于这类场景,我们更多的是去看波动的情况,就是除基线以外的一些特征。今年八月份,百度云开源了一个数据标注的工具-Curve 。我们始终觉得算法虽然很重要,但远没有数据本身重要。做机器学习时,数据的建设才是最需要花时间解决的问题,百度的运维工程师也是重点在解决数据标准和数据获取的问题。如何应对报警风暴当出现大规模报警时,手机可能会直接被打爆。异常检测重点解决的是故障感知的问题。当故障被感知后,需要通知给运维工程师。首先,做逐级通告,对报警进行分级。接着做数据的整理,整理出每一个数据,最后抽象化数据的特征,按照每个维度或特征进行报警的归并。完成前两步之后,报警会有一定改善。最后要用数据分析方法或者机器学习的方法处理。数据的特征已经被抽象化,所以有很多方法可以解决,第一种方法是传统数据挖掘,比如关联分析,频繁项集挖掘是最被广泛使用到的方法,它可以有效将同类报警进行合并。第二种方法是机器学习,因为前面抽象出了特征,那做分类聚类都是比较直接的事情。从我们的实践情况看,最后的效果两者相差不大,大家都可以尝试。报警产生后,就相当于感知阶段结束,之后就到达故障处理阶段。接下来,我分享几个百度内部觉得效果最好的处理方法。第一个方法,多维度定位。这个更多偏业务问题的定位。业务都有访问日志,日志由各个不同维度的数据组成。一个故障的出现可能有不同维度,运维工程师需要通过访问日志的数据进行计算分析,分析出真正影响故障的维度。在这个基础上,可以做可视化。这是一类结合业务特征的可视化方法,如上图,这是一个模块拓扑图,很多圈圈,很多研发,这里有健康度、响应时间等等各种维度的展示。像模块响应时间,又可能会分很多类、很多维度或者很多模块,底下是每一个不同的模块,都可能产生对应的一些情况。接下来,百度现在大部分在用的是基于信息熵的维度特征推荐。例如,一个出现故障问题的指标,大的流量下降,可能有不同的维度。运维工程师会对每一个维度里的子维度数据进行分析,分析下降的程度,以及对于现在整个流量总体的下降程度的不同占比,然后做一个排序,就可以得到故障影响较高的某几个维度,从而帮助工程师尽快定位到这个问题或者缩小问题的范围。第二个方法,基于服务拓扑或者服务关联做定位。这是内部比较重要的故障判断基础和指导意见。百度运维倾向于把一个问题的分析分成六个维度:①时间维度,缩小时间范围;②网络拓扑模型,缩小空间范围,区分整体和局部故障;③服务管理模型,推导异常集群、实例或者机器;④变更关联模型,定位程序、配置、数据、运营活动上线;⑤模块关联模型,上下游关联服务的异常传播链;⑥多维度模型,维度关联层级分析,缩小业务范围。上图是一类典型的故障诊断框架。我们可能有很多故障的分类,比如有网络故障,细分一点是有交换机故障、链路故障,可能有系统故障,业务问题、操作问题等各种各样的,都是属于假说生成,可能都是备选故障问题。中间有一个证据评分,相当于基于前面的模型拓扑关系,对不同的故障做评分,把拓扑关系的线做权重,然后做置信计算和排序,最后给出最优决策判断。有关自愈的问题· 故障自愈 通过自动化、智能化处理故障节省人力投入,通过预设定的处理流程和只能化判断策略,提高故障处理可靠性,同时降低故障时间,为业务可用性保驾护航。· 智能自愈①感知:通过监控系统获取业务运行指标、智能异常检测、网络异常事件多种触发方式②决策:根据不同感知方式可以配置不同决策模型③执行:在单机执行基础上,提供集群级别、分布式的处理方式在执行故障自愈过程中,并不止是一个工具的执行,而是包括了调度、伸缩、隔离预案处理甚至多个不同业务的联动。自愈本身的核心并非自动化过程,更多是决策的过程。举一个典型案例叫单机房故障自愈。单机房,不仅仅指机房网络故障,更多指的是故障范围只要限定在一个IDC内部,不管这个故障是代码BUG,还是外面流量接入出了问题,还是机房整个掉电,只要故障范围是在一个IDC内都可以解决。基础能力达标后,我们要设计一个故障自愈系统,核心部分是外网流量调度止损决策器和内网流量调度止损决策器。外网比较简单,而内网则涉及到一些负载均衡策略、弹性伸缩策略、主备切换策略等。盲测验收最后讲一下盲测验收。有了故障自愈的系统后,怎么证明你的方案好用呢?在不通知业务的情况下,我们会和IDC同事进行配合,拔网线或是制造网络拥塞,这时候才能进行完整的切换,从而可以证明基础能力是否达标。百度现在单机房故障自愈已经覆盖了所有核心业务线,自愈时效控制在5分钟内,并且对于非数据库依赖的业务,可以做到1-2分钟完成机房级自愈。咨询服务场景服务咨询的场景可分为以下三种:①通过聊天窗口(IM软件、浏览器等)实时查询业务状态,用户可视化、可追查各种问题;②通过聊天窗口(IM软件、浏览器等)实时触发运维操作,运维工程师可远程上线、启停任务等;③当运维操作完成,出现状态变化或异常等情况时,运维工程师可主动发送相关通知,或按照策略自动进行后续操作。在百度内部,我们将这种场景称为ChatOps:•“放心”:分级发布和可用性干预、保障•“贴心”:监控、部署一站式集成,信息主动推送和确认•“省心”:高度自动化,减少人工介入和等待•“开心”:助力业务发展,如迭代效率提升•将运维人员从日渐琐碎、枯燥、疲惫、低价值、高事故率的工作中解放出来•实现运维人员的转型和增值AIOps的挑战最后说一下AIOps的挑战。现有的AIOps技术,比如指标异常检测、故障自愈等,更多解决的是数据本身的特征和问题,还没抽象到服务、程序本身的特征这个层次上,也就是说,我们并没有真正地了解和解决问题本身。比如,不同类的服务所产生的故障和表征是不一样的,我们希望让数据更多、业务场景可扩展,而非针对几个横向的场景;在业务运营方面,我们不仅仅局限在IDC、操作系统、机器,而是注重资源和性能优化,运维还可以继续拓展。对内,可以做系统优化、成本优化;对外,帮助所有用户做云服务资源池优化,让大家更好的节约成本,提升服务能力。以上内容来自曲显平老师的分享。声明:本文是由msup原创,转载请联系 meixu.feng@msup.com.cn

December 29, 2018 · 1 min · jiezi

Centos7 安装Nginx整合Lua

前言本人的使用的电脑是Mac,操作系统是macOS Mojave。电脑上装有虚拟机。虚拟机上安装Centos7操作系统,在其之上安装Nginx及Luau类库,整个过程是在系统安装完成之后开始记录。建议安装前先拍快照,出现问题可以恢复准备工作如果安装的Linux能够联网,并且外部也能正常使用Linux的端口,那么可以忽略下面两部1.设置自动获取ip(1)在Linux上输入命令[root@localhost ~]ip addr #查看ip[root@localhost ~]nmcli connection show可以查看当前网卡信息我的是 ens33(2)修改信息[root@localhost ~]vi /etc/sysconfig/network-scripts/ifcfg-ens33将最后一行ONBOOT=no 修改为 ONBOOT=yes (3)重启网络服务[root@localhost ~]# systemctl restart network2.关闭防火墙systemctl stop firewalld.service #停止firewallsystemctl disable firewalld.service #禁止firewall开机启动3.准备安装是发现没有wget命令,可以先按照线面安装如果下面提示没有wget命令时,可以执行这一步[root@localhost ~]#yum -y install wget安装1.安装依赖环境[root@localhost ~]#yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel2.安装LuaJIT我是在/usr/local路径下创建了 LuaJIT 文件夹[root@localhost LuaJIT]#wget http://luajit.org/download/LuaJIT-2.0.2.tar.gz[root@localhost LuaJIT]#tar –xvf LuaJIT-2.0.2.tar.gz[root@localhost LuaJIT]#cd LuaJIT-2.0.2[root@localhost LuaJIT-2.0.2]#make install3.安装nginx(1)下载ngx_devel_kit、lua-nginx-module、nginx我是在/usr/local路径下创建了 nginx 文件夹[root@localhost nginx]#wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz[root@localhost nginx]#wget https://github.com/openresty/lua-nginx-module/archive/v0.10.9rc7.tar.gz[root@localhost nginx]#wget http://nginx.org/download/nginx-1.12.1.tar.gz #注意下载后的压缩包没有文件名称,但是根据版本号能区分是哪个文件[root@localhost nginx]#tar -xvf v0.3.0.tar.gz[root@localhost nginx]#tar -xvf v0.10.9rc7.tar.gz[root@localhost nginx]#tar -xvf nginx-1.12.1.tar.gz(2)编译Nginx[root@localhost nginx]# cd nginx-1.12.1[root@localhost nginx-1.12.1]#./configure –prefix=/usr/local/nginx –add-module=../ngx_devel_kit-0.3.0 –add-module=../lua-nginx-module-0.10.9rc7(3)安装[root@localhost nginx-1.12.1]#make[root@localhost nginx-1.12.1]#make install(4)启动nginx 启动时会nginx可能会报错 ./nginx: error while loading shared libraries: libluajit-5.1.so.2: cannot open shared object file: N找不到libluajit-5.1.so.2这个文件解决办法1.找到 libluajit-5.1.so.2,libluajit-5.1.so.2.0.2这两个文件复制到 对应的lib下 64位是 /usr/lib64 32位是 /usr/lib[root@localhost nginx-1.12.1]#find / -name libluajit-5.1.so.2发现文件默认是安装在 /usr/local/lib/libluajit-5.1.so.2下[root@localhost nginx-1.12.1]#cp /usr/local/lib/libluajit-5.1.so.2 /usr/lib64/[root@localhost nginx-1.12.1]#cp /usr/local/lib/libluajit-5.1.so.2.0.2 /usr/lib64在nginx安装目录下,修改nginx.conf文件在Server代码块下添加如下代码location /hello{ default_type ’text/plain’; content_by_lua ’ngx.say(“hello,lua”)’; }启动nginx[root@localhost nginx-1.12.1]#./configure在浏览器访问 虚拟对应的地址 http://xxx.xxx.xxx/hello显示如下到此就成功了 ...

December 24, 2018 · 1 min · jiezi

Mac环境安装Lua

准备工作,需要下载lua官网地址http://www.lua.org/download.html,我下载的是最新版的lua-5.3.5.tar.gz下载后解压。运行终端,进入解压后的文件夹,1.执行命令 make macosx2.执行命令 make test这时候会显示如下结果:src/lua -vLua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio3.执行命令 sudo make install,这个时候会要求输入密码,输入密码后就开始安装了需要注意的是:lua安装的位置是在 /usr/local/bin/ 下 而非是/usr/bin,这个有个问题会在下面出现。开发工具我使用的是Sublime Text3,网上搜索大部分都是用这个。

December 24, 2018 · 1 min · jiezi

lsyncd —— 多机器实时同步文件神器

lsyncd 是一个支持实时、双向、多机器的多模式文件同步工具。使用 Lua 语言封装了 inotify 和 rsync 工具,采用了 Linux 内核(2.6.13 及以后)里的 inotify 触发机制,然后通过 rsync 去差异同步,达到实时的效果。安装在源文件服务器上安装:yum -y install lsyncd配置lsyncd 主配置文件,假设放置在/etc/lsyncd.conf:settings { nodaemon = false, logfile = “/var/log/lsyncd.log”, statusFile = “/var/log/lsyncd.status”, inotifyMode = “CloseWrite”, maxProcesses = 8}– 可以有多个sync,各自的source,各自的target,各自的模式,互不影响。sync { default.rsyncssh, source = “/home/wwwroot/web1/”, host = “111.222.333.444”, targetdir = “/home/wwwroot/web1/”, – 忽略文件路径规则,可用table也可用外部配置文件 – excludeFrom = “/etc/lsyncd_exclude.lst”, exclude = { “.svn”, “Runtime/”, “Uploads/”, }, – maxDelays = 5, delay = 0, – init = false, rsync = { binary = “/usr/bin/rsync”, archive = true, compress = true, verbose = true, _extra = {"–bwlimit=2000"}, },}忽略规则需要忽略同步的文件或文件夹,excludeFrom 选项才配置该文件,exclude 类型的配置不用该配置文件。假设配置文件放在/etc/lsyncd_exclude.lst。.svnRuntime/**Uploads/**免密登录为避免每次都需要手动输入密码,可设置为 SSH 免密登录。启动lsyncd -log Exec /etc/lsyncd.conf参考官方 Wikilsyncd实时同步搭建指南——取代rsync+inotify原文地址: https://shockerli.net/post/li… ...

December 13, 2018 · 1 min · jiezi

socket踩坑实录

socket简述socket(双工协议)网络中的两个程序,通过一个双向的连接来实现数据的交换,我们把连接的一端称为socketsocket特性自带连接保持可以实现双向通信socket分类基于TCP的socket基于UDP的socket基于RawIP的socket基于链路层的socket文章持续更新中~~~~

November 25, 2018 · 1 min · jiezi

大佬带你深入浅出Lua虚拟机

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦本文由鹅厂优文发表于云+社区专栏作者:郑小辉 | 腾讯 游戏客户端开发高级工程师写在前面:本文所有的文字都是我手工一个一个敲的,以及本文后面分享的Demo代码都是我一行一行码的,在我之前已经有非常多的前辈研究过Lua虚拟机了,所以本文很多思想必然是踏在这些巨人的肩膀上的。 本文标题是”深入浅出Lua虚拟机”,其实重点在浅出这两字上。毕竟作者的技术水平有限。但是听说名字要起的屌一点文章才有人看,故而得名。 谨以此文奉献给那些对Lua虚拟机有兴趣的人。希望本文能达到一个抛砖引玉的效果。Lua的执行流程:Lua代码的整个流程:如下图所示:程序员编码lua文件->语法词法分析生成Lua的字节码文件(对应Lua工具链的Luac.exe)->Lua虚拟机解析字节码,并执行其中的指令集->输出结果。蓝色和绿色的部分是本文所试图去讲的内容。词法语法分析: 我不准备讲Lua的所有词法分析过程,毕竟如果浪费太多时间来写这个的话一会策划同学要提刀来问我需求的开发进度如何了,所以长话短说,我就根据自己对Lua的理解,以某一个具体的例子来做分析: Lua代码块: If a < b then a = c end 这句话咱们程序员能看懂,可是计算机就跟某些男程序员家里负责貌美如花的老婆一样,只知道这是一串用英文字符拼出来的一行没有任何意义的字符串而已。 为了让计算机能够读懂这句话,那么我们要做的第一件事情就是分词:既然你看不懂。我就先把一句话拆成一个一个单词,而且我告诉你每个单词的含义是什么。 分词的结果大概长下面这样: 分词结果 类型(意义) if Type_If (if 关键字) a Type_Var (这是一个变量) < Type_OpLess(这是一个小于号) b Type_Var(这是一个变量) then Type_Then(Then关键字) a Type_Var (这是一个变量) = Type_OpEqual(这是一个等号) c Type_Var(这是一个变量) end Type_End(End关键字) 好了。现在计算机终于明白了。原来你写的这行代码里面有9个字,而且每个字的意思我都懂了。所以现在问题是,计算机理解了这句话了吗? 计算机依然不理解。就好像“吃饭”这句话,计算机理解了 “吃”是动词,张开嘴巴的意思。“饭”是名词,指的米饭的意思。但是你把吃饭放在一起,计算机并不知道这是“张开嘴巴,把饭放进嘴里,并且咽到胃里”的意思。因为计算机只知道“张开嘴巴”和“米饭”两件事,这两件事有什么联系,计算机并不能理解。有人会说了:简单:吃+其他字 这种结构就让计算机笼统的理解为把后一个词代表的东西放进嘴巴里的意思就好了啊?这种情况适合”吃饭”这个词,但是如果这样你让计算机怎么理解“吃惊”这个词呢?所以这里引出下一个话题:语义解析。 关于语义解析这块,如果大家想要了解的更深入,可以去了解一下AST(抽象语法树)。然而对于我们这个例子,我们用简单的方式模拟着去理解就好了。 对于Lua而言,每一个关键字都有自己特别的结构。所以Lua的关键字将成为语义解析的重点。我们现在涉及到的if这个例子:我们可以简单的用伪代码表述这个解析过程: 对于if语句我们可以抽象成这种结构: If condition(条件表达式) then dosth(语句块) end 所以对if语句块进行解析的伪代码如下: ReadTokenWord(); If(tokenWord.type == Type_If) then ReadCondition() //读取条件表达式 ReadThen() //读取关键字then ReadCodeBlock() //读取逻辑代码块 ReadEnd() //读取关键字End End所以为了让计算机理解,我们还是得把这个东西变成数据结构。 因为我只是做一个Demo而已,所以我用了先验知识。也就是我假定我们的If语句块逻辑结构是这样的: If 小于条件表达式 then 赋值表达式 End 所以在我的Demo里转成C++数据结构就是IfStateMent大概是这样: OK,所以现在,我们整个词法语法分析都做完了。但是真正的Lua虚拟机并不能执行我们的ifStateMent这种东西。Lua源码里的实现也是类似这种TokenType 和 结构化的 if Statement whileStatement等等,并且Lua没有生成完整的语法树。Lua源码的实现里面,它是解析一些语句,生成临时的语法树,然后翻译成指令集的。并不会等所有的语句都解析完了再翻译的。语义解析和翻译成指令集是并行的一个过程。贴一个源码里面关于语义解析的部分实现: OK,现在咱们已经把我们程序员输入的Lua代码变成了一个数据结构(计算机能读懂)。下一步我们要把这个数据结构再变成Lua虚拟机能认识的东西,这个东西就是 Lua 指令集! 至于转换的过程,对于我们这个例子,大概是这样的: If a < b then a = c end 先理解条件 a<b:一种基于寄存器的指令设计大概是这样的: a,b均为变量。假定我们的可用的寄存器索引值从10(0-9号寄存器都已经被占用了)开始:又假定我们有一个常量索引表:0号常量:字符’a’,1号常量:字符串’b’。那么a<b可以被翻译为这样:LoadK 10,0 :将_G[ConstVar[0]]载入10号寄存器: R[10] = _G[“a”]LoadK 11,1 :将_G[ConstVar[1]]载入11号寄存器: R[11] = _G[“b”]LT 10,11 : 比较R[10]<R[11]是否成立,如果成立,则跳过下一条指令(++PC),否则执行下一条指令。LT后面跟着的一条指令必然是JMP指令。就是如果R[10]<R[11]成立,则不执行JMP,直接执行JMP后面的一条指令(a=c的语句块对应的指令集),否则直接跳过下面的一个语句块(跳过a=c的赋值过程)。 同理,继续进行a=c的翻译等等。 所以If a < b then a = c end在我写的demo里面最后被翻译成了: OK,我们现在大概明白了从Lua代码怎么变成指令集的这件事了。 现在我们来具体看一下Lua5.1的指令集: Lua的指令集是定长的,每一条指令都是32位,其中大概长这样: 每一条指令的低六位 都是指令的指令码,比如 0代表MOVE,12代表Add。Lua总共有37条指令,分别是MOVE,LOADK,LOADBOOL,LOADNIL,GETUPVAL,GETGLOBAL,GETTABLE,SETGLOBAL,SETUPVAL,SETTABLE,NEWTABLE,SELF,ADD,SUB,MUL,DIV,MOD,POW,UNM,NOT,LEN,CONCAT,JMP,EQ,LT,LE,TEST,TESTSET,CALL,TAILCALL,RETURN,FORLOOP,TFORLOOP,SETLIST,CLOSE,CLOSURE,VARARG. 我们发现图上还有iABC,iABx,iAsBx。这个意思是有的指令格式是 OPCODE,A,B,C的格式,有的指令是OPCODE A,BX格式,有的是OPCODE A,sBX格式。sBx和bx的区别是bx是一个无符号整数,而sbx表示的是一个有符号的数,也就是sbx可以是负数。 我不打算详细的讲每一条指令,我还是举个例子: 指令编码 0x 00004041 这条指令怎么解析: 0x4041 = 0000 0000 0000 0000 0100 0000 0100 0001 低六位(05)是opcode:000001 = 1 = LoadK指令(037分别对应了我上面列的38条指令,按顺序来的,0是Move,1是loadk,2是loadbool…..37是vararg)。LoadK指令格式是iABC(C没用上,仅ab有用)格式。所以我们再继续读ab。 a = 低613位 为 00000001 = 1所以a=1 b = 低1422位 为000000001 = 1所以b=1 所以0x4041 = LOADK 1, 1 指令码如何解析我也在demo里面写了,代码大概是这样: 那么Lua文件经过Luac的编译后生成的Lua字节码,Lua字节码文件里面除了包含指令集之外又有哪些东西呢?当然不会像我上面的那个词法语法解析那个demo那么弱智拉。所以下面我们就讲一下Lua字节码文件的结构: Lua字节码文件(*.lua.bytes)包含了:文件头+顶层函数: 文件头结构:顶层函数和其他普通函数都拥有同样的结构: 所以我们是可以轻松自己写代码去解析的。后文提供的Demo源码里面我也已经实现了字节码文件的解析。Demo中的例子是涉及到的Lua源代码以及最终解析字节码得到的信息分别是: OK,本文现在就剩最后一点点东西了:Lua虚拟机是怎么执行这些指令的呢? 大概是这样的: While(指令不为空) 执行指令 取下一条要执行的指令 End 每一条指令应该怎么执行呢???如果大家还有印象的话,咱们前文语义解析完之后转指令集是这样的:a < bLoadK 10,0 :将_G[ConstVar[0]]载入10号寄存器: R[10] = _G[“a”]LoadK 11,1 :将_G[ConstVar[1]]载入11号寄存器: R[11] = _G[“b”]LT 10,11 : 比较R[10]<R[11]是否成立,如果成立,则跳过下一条指令(++PC),否则执行下一条指令。LT后面跟着的一条指令必然是JMP指令。就是如果R[10]<R[11]成立,则不执行JMP,直接执行JMP后面的一条指令(a=c的语句块),否则直接跳过下面的一个语句块(跳过a=c的赋值过程)。那当然是指令后面的文字就已经详细的描述了指令的执行逻辑拉,嘿嘿。为了真正的执行起来,所以我们在数据结构上设计需要 1,寄存器:2,常量表:3,全局变量表:为了能执行我们demo里面的例子:我实现了这段代码涉及到的所有指令insExecute[(int)OP_LOADK] = &LuaVM::LoadK;insExecute[(int)OP_SETGLOBAL] = &LuaVM::SetGlobal;insExecute[(int)OP_GETGLOBAL] = &LuaVM::GetGlobal;insExecute[(int)OP_ADD] = &LuaVM::_Add;insExecute[(int)OP_SUB] = &LuaVM::_Sub;insExecute[(int)OP_MUL] = &LuaVM::_Mul;insExecute[(int)OP_DIV] = &LuaVM::_Div;insExecute[(int)OP_CALL] = &LuaVM::_Call;insExecute[(int)OP_MOD] = &LuaVM::_Mod;insExecute[(int)OP_LT] = &LuaVM::_LT;insExecute[(int)OP_JMP] = &LuaVM::_JMP;insExecute[(int)OP_RETURN] = &LuaVM::_Return;以Add为例:bool LuaVM::_Add(LuaInstrunction ins){ //R(A):=RK(B)+RK(C) ::: //Todo:必要的参数合法性检查:如果有问题则抛异常 // 将ins.bValue代表的数据和ins.cValue代表的数据相加的结果赋值给索引值为ins.aValue的寄存器 luaRegisters[ins.aValue].SetValue(0, GetBK(ins.bValue) + GetBK(ins.cValue)); return true;}下面是程序的运行效果截图: 看完整个过程,其实可以思考这个问题:为什么Lua执行效率会远远低于C程序? 个人愚见: 1,真假寄存器:Lua指令集涉及到的寄存器是模拟的寄存器,其实质还是内存上的一个数据。访问速度取决于CPU对内存的访问速度。而C程序最后可以用win32指令集or Arm指令集来执行。这里面涉及到的寄存器EBX,ESP等都是CPU上面的与非门,其访问速度=CPU的频率(和cpu访问内存的速度对比简直一个天上一个地上)。 2,指令集运行的平台:Lua指令集运行的平台是Lua虚拟机。而C程序指令集运行的直接是硬件支持的。 3,C里面的数据直接对应的就是内存地址。而Lua里面的数据对应的是一个描述这个数据的数据结构。所以隔了这么一层,效率也大打折扣了。 4,比如Lua的Gc操作等等这些东西都是C程序不需要去做的。。。。 OK,最后献上我写的这个demo的源代码:这份源代码是我在清明节在家的时候瞎写的。也就是说代码并没有经过耐心的整理,而且清明节有人找我出去喝酒,导致我有很长一段时间都处于“我艹快点码完我要出去喝了”这种心不在焉的状态,所以有些编码格式和结构设计都处处能看到随性的例子毕竟只是一个demo嘛。人生在世,要有佛性,随缘就好!如果各位真的想进一步理解关于Lua虚拟机的东西,那么我推荐诸位有空耐着性子去读一读Lua虚拟机的源代码~ 最后,诚挚感谢所有看到了最后这句话的同学。谢谢你们耐着性子看完了一个技术菜鸡的长篇废话。Demo.zip问答Lua支持Unicode吗?相关阅读Lua 性能剖析使用lua小技巧Lua 游戏开发学习 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 25, 2018 · 2 min · jiezi