Locations of visitors to this page

Tuesday, August 16, 2011

MySQL Proxy RW Splitting

MySQL Proxy RW Splitting

=rw-splitting.lua=

==说明==
* MySQL Proxy 0.8.1版本的读写分离脚本
* 下载地址
mysql-proxy-0.8.1-linux-glibc2.3-x86-64bit.tar.gz
* 参考文档
** 读写分离脚本说明
MySQL Proxy RW Splitting: http://forge.mysql.com/wiki/MySQL_Proxy_RW_Splitting
MySQL Proxy learns R/W Splitting: http://jan.kneschke.de/projects/mysql/mysql-proxy-learns-r-w-splitting
MySQL Proxy: more R/W splitting: http://jan.kneschke.de/projects/mysql/mysql-proxy-more-r-w-splitting/
** Lua开发
Lua 5.1 Reference Manual: http://www.lua.org/manual/5.1/manual.html
Programming in Lua (first edition): http://www.lua.org/pil/
** MySQL Proxy脚本开发
MySQL Proxy Scripting: http://dev.mysql.com/doc/refman/5.5/en/mysql-proxy-scripting.html
MySQL Proxy Snippet: http://forge.mysql.com/tools/search.php?t=tag&k=mysqlproxy
MySQL Proxy Dev: http://forge.mysql.com/wiki/MySQL_Proxy_Dev
Advanced LUA Scripting: http://forge.mysql.com/wiki/Advanced_LUA_Scripting
ProxyCookbook: http://forge.mysql.com/wiki/ProxyCookbook
http://blog.suncrescent.net/2008/05/partial-mysql-proxy-api-doc/

==代码==
===1-20行===
  1 --[[ $%BEGINLICENSE%$
  2  Copyright (c) 2007, 2009, Oracle and/or its affiliates. All rights reserved.
  3
  4  This program is free software; you can redistribute it and/or
  5  modify it under the terms of the GNU General Public License as
  6  published by the Free Software Foundation; version 2 of the
  7  License.
  8
  9  This program is distributed in the hope that it will be useful,
 10  but WITHOUT ANY WARRANTY; without even the implied warranty of
 11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12  GNU General Public License for more details.
 13
 14  You should have received a copy of the GNU General Public License
 15  along with this program; if not, write to the Free Software
 16  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 17  02110-1301  USA
 18
 19  $%ENDLICENSE%$ --]]
 20

* 声明版权信息
* lua脚本中 "--\[\[" 和 "--\]\]" 之间表示多行注释块

===21-29行===
 21 ---
 22 -- a flexible statement based load balancer with connection pooling
 23 --
 24 -- * build a connection pool of min_idle_connections for each backend and maintain
 25 --   its size
 26 -- *
 27 --
 28 --
 29

* 脚本说明
* lua脚本中 "--" 之后表示单行注释信息

===30-34行===
 30 local commands    = require("proxy.commands")
 31 local tokenizer   = require("proxy.tokenizer")
 32 local lb          = require("proxy.balance")
 33 local auto_config = require("proxy.auto-config")
 34

* require加载MySQL Proxy模块
* 模块文件所在缺省目录是$LUA_HOME/lib/mysql-proxy/lua/proxy/
* 各模块说明如下:
** commands.lua:解析网络包中的MySQL命令
*** 参考 http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Command_Packet_.28Overview.29
** tokenizer.lua:加载模块mysql.tokenizer;格式化SQL语句。没看懂
** balance.lua:负载均衡
*** function idle_failsafe_rw:获取一个空闲的可读写的连接(可读写的主库或备库)
*** function idle_ro:获取一个空闲的只读的连接(只读备库)
** auto-config.lua:加载模块lpeg,即"Parsing Expression Grammars For Lua";管理配置参数
*** 语法
**** proxy show config 显示配置
**** proxy set global xxx.yyy=zzz 设置配置
**** proxy save config into "xxx" 保存配置
**** proxy load config from "xxx" 加载配置
*** 参考
**** http://www.inf.puc-rio.br/~roberto/lpeg/
**** http://www.gammon.com.au/scripts/doc.php?general=lua_lpeg
*** 问题
**** [[#proxy set global 报错 ERROR 1064 (42000): You have an error in your SQL syntax]]
**** [[#proxy set global 报错 ERROR 1105 (07000): (lua) proxy.response.errmsg is nil]]
**** [[#proxy save config 保存配置文件大小等于0]]
**** [[#proxy save config 保存配置文件名带双引号]]

===35-46行===
 35 --- config
 36 --
 37 -- connection pool
 38 if not proxy.global.config.rwsplit then
 39         proxy.global.config.rwsplit = {
 40                 min_idle_connections = 4,
 41                 max_idle_connections = 8,
 42
 43                 is_debug = false
 44         }
 45 end
 46

* 设置读写分离的缺省配置参数

===47-55行===
 47 ---
 48 -- read/write splitting sends all non-transactional SELECTs to the slaves
 49 --
 50 -- is_in_transaction tracks the state of the transactions
 51 local is_in_transaction       = false
 52
 53 -- if this was a SELECT SQL_CALC_FOUND_ROWS ... stay on the same connections
 54 local is_in_select_calc_found_rows = false
 55

* 初始化变量
* is_in_transaction:false表示转发查询语句到只读连接上
* is_in_select_calc_found_rows:表示查询语句中是否有"SQL_CALC_FOUND_ROWS"

===56-72行===
 56 ---
 57 -- get a connection to a backend
 58 --
 59 -- as long as we don't have enough connections in the pool, create new connections
 60 --
 61 function connect_server()
 62         local is_debug = proxy.global.config.rwsplit.is_debug
 63         -- make sure that we connect to each backend at least ones to
 64         -- keep the connections to the servers alive
 65         --
 66         -- on read_query we can switch the backends again to another backend
 67
 68         if is_debug then
 69                 print()
 70                 print("[connect_server] " .. proxy.connection.client.src.name)
 71         end
 72

* 函数connect_server说明: 获取一个到后端的连接
* 初始化is_debug变量

===75-121行===
 73         local rw_ndx = 0
 74
 75         -- init all backends
 76         for i = 1, #proxy.global.backends do
 77                 local s        = proxy.global.backends[i]
 78                 local pool     = s.pool -- we don't have a username yet, try to find a connections which is idling
 79                 local cur_idle = pool.users[""].cur_idle_connections
 80
 81                 pool.min_idle_connections = proxy.global.config.rwsplit.min_idle_connections
 82                 pool.max_idle_connections = proxy.global.config.rwsplit.max_idle_connections
 83
 84                 if is_debug then
 85                         print("  [".. i .."].connected_clients = " .. s.connected_clients)
 86                         print("  [".. i .."].pool.cur_idle     = " .. cur_idle)
 87                         print("  [".. i .."].pool.max_idle     = " .. pool.max_idle_connections)
 88                         print("  [".. i .."].pool.min_idle     = " .. pool.min_idle_connections)
 89                         print("  [".. i .."].type = " .. s.type)
 90                         print("  [".. i .."].state = " .. s.state)
 91                 end
 92
 93                 -- prefer connections to the master
 94                 if s.type == proxy.BACKEND_TYPE_RW and
 95                    s.state ~= proxy.BACKEND_STATE_DOWN and
 96                    cur_idle < pool.min_idle_connections then
 97                         proxy.connection.backend_ndx = i
 98                         break
 99                 elseif s.type == proxy.BACKEND_TYPE_RO and
100                        s.state ~= proxy.BACKEND_STATE_DOWN and
101                        cur_idle < pool.min_idle_connections then
102                         proxy.connection.backend_ndx = i
103                         break
104                 elseif s.type == proxy.BACKEND_TYPE_RW and
105                        s.state ~= proxy.BACKEND_STATE_DOWN and
106                        rw_ndx == 0 then
107                         rw_ndx = i
108                 end
109         end
110
111         if proxy.connection.backend_ndx == 0 then
112                 if is_debug then
113                         print("  [" .. rw_ndx .. "] taking master as default")
114                 end
115                 proxy.connection.backend_ndx = rw_ndx
116         end
117
118         -- pick a random backend
119         --
120         -- we someone have to skip DOWN backends
121

* 创建一个新的或分配一个已有的MySQL Proxy到MySQL Server后端的连接
* 按backends顺序和下列次序选择:
** 优先选取一个可读写的后端,直到该后端空闲连接数达到最小空闲连接数
** 其次选取一个只读的后端,直到该后端空闲连接数达到最小空闲连接数
** 最后选取第一个可读写的后端
* backends顺序是:先rw后ro;命令行配置中出现的先后顺序
* 问题:分配算法和实际测试结果有差异,原因不明。比如min_idle_connections设置是1,但可以建min_idle_connections+1=2个连接
* connected_clients总是0,原因不明,此变量需要脚本自行赋值维护?
* 当客户端连接数小于min_idle_connections+1时,调试信息中cur_idle_connections总显示为0。超过min_idle_connections+1后才显示出数值,不会新建连接,等于min_idle_connections+1

===122-139行===
122         -- ok, did we got a backend ?
123
124         if proxy.connection.server then
125                 if is_debug then
126                         print("  using pooled connection from: " .. proxy.connection.backend_ndx)
127                 end
128
129                 -- stay with it
130                 return proxy.PROXY_IGNORE_RESULT
131         end
132
133         if is_debug then
134                 print("  [" .. proxy.connection.backend_ndx .. "] idle-conns below min-idle")
135         end
136
137         -- open a new connection
138 end
139

* 没看懂。估计是,如果已建立了连接池,则返回PROXY_IGNORE_RESULT,表示忽略接下来服务器发回的handshake,这样就不会建立起新连接
* 函数connect_server结束

===140-164行===
140 ---
141 -- put the successfully authed connection into the connection pool
142 --
143 -- @param auth the context information for the auth
144 --
145 -- auth.packet is the packet
146 function read_auth_result( auth )
147         if is_debug then
148                 print("[read_auth_result] " .. proxy.connection.client.src.name)
149         end
150         if auth.packet:byte() == proxy.MYSQLD_PACKET_OK then
151                 -- auth was fine, disconnect from the server
152                 proxy.connection.backend_ndx = 0
153         elseif auth.packet:byte() == proxy.MYSQLD_PACKET_EOF then
154                 -- we received either a
155                 --
156                 -- * MYSQLD_PACKET_ERR and the auth failed or
157                 -- * MYSQLD_PACKET_EOF which means a OLD PASSWORD (4.0) was sent
158                 print("(read_auth_result) ... not ok yet");
159         elseif auth.packet:byte() == proxy.MYSQLD_PACKET_ERR then
160                 -- auth failed
161         end
162 end
163
164

* 参考
** http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol
* 问题
** [[#read_auth_result没有显示调试信息]]

===165-171行===
165 ---
166 -- read/write splitting
167 function read_query( packet )
168         local is_debug = proxy.global.config.rwsplit.is_debug
169         local cmd      = commands.parse(packet)
170         local c        = proxy.connection.client
171

* read_query函数
* 解析网络包、语句

===172-174行===
172         local r = auto_config.handle(cmd)
173         if r then return r end
174

* 处理PROXY命令(show,set,save,load),返回PROXY_SEND_RESULT和结果

===175-188行===
175         local tokens
176         local norm_query
177
178         -- looks like we have to forward this statement to a backend
179         if is_debug then
180                 print("[read_query] " .. proxy.connection.client.src.name)
181                 print("  current backend   = " .. proxy.connection.backend_ndx)
182                 print("  client default db = " .. c.default_db)
183                 print("  client username   = " .. c.username)
184                 if cmd.type == proxy.COM_QUERY then
185                         print("  query             = "        .. cmd.query)
186                 end
187         end
188

* 声明本地变量
* 显示调试信息

===189-204行===
189         if cmd.type == proxy.COM_QUIT then
190                 -- don't send COM_QUIT to the backend. We manage the connection
191                 -- in all aspects.
192                 proxy.response = {
193                         type = proxy.MYSQLD_PACKET_OK,
194                 }
195
196                 if is_debug then
197                         print("  (QUIT) current backend   = " .. proxy.connection.backend_ndx)
198                 end
199
200                 return proxy.PROXY_SEND_RESULT
201         end
202
203         proxy.queries:append(1, packet, { resultset_is_needed = true })
204

* 截取QUIT命令,不发送到后端服务器
* 构造查询队列。命令放入查询队列

===205-255行===
205         -- read/write splitting
206         --
207         -- send all non-transactional SELECTs to a slave
208         if not is_in_transaction and
209            cmd.type == proxy.COM_QUERY then
210                 tokens     = tokens or assert(tokenizer.tokenize(cmd.query))
211
212                 local stmt = tokenizer.first_stmt_token(tokens)
213
214                 if stmt.token_name == "TK_SQL_SELECT" then
215                         is_in_select_calc_found_rows = false
216                         local is_insert_id = false
217
218                         for i = 1, #tokens do
219                                 local token = tokens[i]
220                                 -- SQL_CALC_FOUND_ROWS + FOUND_ROWS() have to be executed
221                                 -- on the same connection
222                                 -- print("token: " .. token.token_name)
223                                 -- print("  val: " .. token.text)
224
225                                 if not is_in_select_calc_found_rows and token.token_name == "TK_SQL_SQL_CALC_FOUND_ROWS" then
226                                         is_in_select_calc_found_rows = true
227                                 elseif not is_insert_id and token.token_name == "TK_LITERAL" then
228                                         local utext = token.text:upper()
229
230                                         if utext == "LAST_INSERT_ID" or
231                                            utext == "@@INSERT_ID" then
232                                                 is_insert_id = true
233                                         end
234                                 end
235
236                                 -- we found the two special token, we can't find more
237                                 if is_insert_id and is_in_select_calc_found_rows then
238                                         break
239                                 end
240                         end
241
242                         -- if we ask for the last-insert-id we have to ask it on the original
243                         -- connection
244                         if not is_insert_id then
245                                 local backend_ndx = lb.idle_ro()
246
247                                 if backend_ndx > 0 then
248                                         proxy.connection.backend_ndx = backend_ndx
249                                 end
250                         else
251                                 print("   found a SELECT LAST_INSERT_ID(), staying on the same backend")
252                         end
253                 end
254         end
255

* 没看懂
** 查看查询语句中是否含有SQL_CALC_FOUND_ROWS或LAST_INSERT_ID
** 如果没有LAST_INSERT_ID,则选一个空闲的只读后端,否则后端不变

===256-272行===
256         -- no backend selected yet, pick a master
257         if proxy.connection.backend_ndx == 0 then
258                 -- we don't have a backend right now
259                 --
260                 -- let's pick a master as a good default
261                 --
262                 proxy.connection.backend_ndx = lb.idle_failsafe_rw()
263         end
264
265         -- by now we should have a backend
266         --
267         -- in case the master is down, we have to close the client connections
268         -- otherwise we can go on
269         if proxy.connection.backend_ndx == 0 then
270                 return proxy.PROXY_SEND_QUERY
271         end
272

* 其它非查询语句,选择一个可读写后端
* 如无可选后端,不做后续操作,直接跳出函数

===273-302行===
273         local s = proxy.connection.server
274
275         -- if client and server db don't match, adjust the server-side
276         --
277         -- skip it if we send a INIT_DB anyway
278         if cmd.type ~= proxy.COM_INIT_DB and
279            c.default_db and c.default_db ~= s.default_db then
280                 print("    server default db: " .. s.default_db)
281                 print("    client default db: " .. c.default_db)
282                 print("    syncronizing")
283                 proxy.queries:prepend(2, string.char(proxy.COM_INIT_DB) .. c.default_db, { resultset_is_needed = true })
284         end
285
286         -- send to master
287         if is_debug then
288                 if proxy.connection.backend_ndx > 0 then
289                         local b = proxy.global.backends[proxy.connection.backend_ndx]
290                         print("  sending to backend : " .. b.dst.name);
291                         print("    is_slave         : " .. tostring(b.type == proxy.BACKEND_TYPE_RO));
292                         print("    server default db: " .. s.default_db)
293                         print("    server username  : " .. s.username)
294                 end
295                 print("    in_trans        : " .. tostring(is_in_transaction))
296                 print("    in_calc_found   : " .. tostring(is_in_select_calc_found_rows))
297                 print("    COM_QUERY       : " .. tostring(cmd.type == proxy.COM_QUERY))
298         end
299
300         return proxy.PROXY_SEND_QUERY
301 end
302

* 保证后端数据库和客户端数据库一致
* 显示调试信息

===303-332行===
303 ---
304 -- as long as we are in a transaction keep the connection
305 -- otherwise release it so another client can use it
306 function read_query_result( inj )
307         local is_debug = proxy.global.config.rwsplit.is_debug
308         local res      = assert(inj.resultset)
309         local flags    = res.flags
310
311         if inj.id ~= 1 then
312                 -- ignore the result of the USE 
313                 -- the DB might not exist on the backend, what do do ?
314                 --
315                 if inj.id == 2 then
316                         -- the injected INIT_DB failed as the slave doesn't have this DB
317                         -- or doesn't have permissions to read from it
318                         if res.query_status == proxy.MYSQLD_PACKET_ERR then
319                                 proxy.queries:reset()
320
321                                 proxy.response = {
322                                         type = proxy.MYSQLD_PACKET_ERR,
323                                         errmsg = "can't change DB ".. proxy.connection.client.default_db ..
324                                                 " to on slave " .. proxy.global.backends[proxy.connection.backend_ndx].dst.name
325                                 }
326
327                                 return proxy.PROXY_SEND_RESULT
328                         end
329                 end
330                 return proxy.PROXY_IGNORE_RESULT
331         end
332

* 处理COM_INIT_DB命令的返回结果
* 忽略其它语句的结果

===333-348行===
333         is_in_transaction = flags.in_trans
334         local have_last_insert_id = (res.insert_id and (res.insert_id > 0))
335
336         if not is_in_transaction and
337            not is_in_select_calc_found_rows and
338            not have_last_insert_id then
339                 -- release the backend
340                 proxy.connection.backend_ndx = 0
341         elseif is_debug then
342                 print("(read_query_result) staying on the same backend")
343                 print("    in_trans        : " .. tostring(is_in_transaction))
344                 print("    in_calc_found   : " .. tostring(is_in_select_calc_found_rows))
345                 print("    have_insert_id  : " .. tostring(have_last_insert_id))
346         end
347 end
348

* 参考:http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#OK_Packet
* 检查是否在事务内,是否有insert_id。设置backend_ndx=0,下次重新选取后端连接?

===349-364行===
349 ---
350 -- close the connections if we have enough connections in the pool
351 --
352 -- @return nil - close connection
353 --         IGNORE_RESULT - store connection in the pool
354 function disconnect_client()
355         local is_debug = proxy.global.config.rwsplit.is_debug
356         if is_debug then
357                 print("[disconnect_client] " .. proxy.connection.client.src.name)
358         end
359
360         -- make sure we are disconnection from the connection
361         -- to move the connection into the pool
362         proxy.connection.backend_ndx = 0
363 end
364

* 不懂

==问题==

===ERROR 1047 (08S01): Unknown command===
现象:
* 用MySQL Proxy 0.8.1缺省的rw-splitting脚本,客户端连接多次,当每个backend都建立起一定空闲连接(连接数都达到min_idle_connects+1,原因不清)后,再次连接时报错"ERROR 1047 (08S01): Unknown command"。如下:
$ ./mysql -h 127.0.0.1 -P 4040 -u yyyy -pxxxx -e "proxy show config"
+---------+----------------------+-------+---------+
| module  | option               | value | type    |
+---------+----------------------+-------+---------+
| rwsplit | min_idle_connections | 1     | number  |
| rwsplit | is_debug             | true  | boolean |
| rwsplit | max_idle_connections | 4     | number  |
+---------+----------------------+-------+---------+
$ ./mysql -h 127.0.0.1 -P 4040 -u yyyy -pxxxx -e "proxy show config"
+---------+----------------------+-------+---------+
| module  | option               | value | type    |
+---------+----------------------+-------+---------+
| rwsplit | min_idle_connections | 1     | number  |
| rwsplit | is_debug             | true  | boolean |
| rwsplit | max_idle_connections | 4     | number  |
+---------+----------------------+-------+---------+
$ ./mysql -h 127.0.0.1 -P 4040 -u yyyy -pxxxx -e "proxy show config"
ERROR 1047 (08S01): Unknown command

* MySQL数据库版本是5.5的,改成5.1问题消失

原因:
* 好像认证有问题,5.5下read_auth_result了2次。原因待查

If a long-password capable client tries to authenticate to a server that supports long passwords, but the user password provided is actually short, read_auth_result() will be called twice. The first time, auth.packet:byte() will equal 254, indicating that the client should try again using the old password protocol. The second time time read_auth_result()/ is called, auth.packet:byte() will indicate whether the authentication actually succeeded.


解决:
* 无
* 不用5.5版本的数据库

===read_auth_result没有显示调试信息===
现象:
* is_debug设置为true,然而read_auth_result没有显示出debug信息

原因:
* 脚本问题,函数read_auth_result中的变量is_debug没有赋值

解决:
* 在147行之前增加如下代码:
        local is_debug = proxy.global.config.rwsplit.is_debug

* 修改脚本后即可生效,不用重启MySQL Proxy

===proxy set global 报错 ERROR 1064 (42000): You have an error in your SQL syntax===
现象:
* 客户端执行命令"proxy set global xxx.yyy = zzz",设置MySQL Proxy参数,报语法错误,如下:
mysql> proxy set global rwsplit.min_idle_connections = 2;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'proxy set global rwsplit.min_idle_connections = 2' at line 1


原因:
* auto-config.lua脚本问题。不能处理带下划线的参数名称。

解决:
* 修改脚本lib/mysql-proxy/lua/proxy/auto-config.lua

local literal = l.R("az", "AZ") ^ 1

改成
local literal = l.R("az", "AZ", "__") ^ 1

* 然后重启MySQL Proxy进程

===proxy set global 报错 ERROR 1105 (07000): (lua) proxy.response.errmsg is nil===
现象:
* 设置is_debug=false报错,如下:
mysql> proxy set global rwsplit.is_debug = false;
ERROR 1105 (07000): (lua) proxy.response.errmsg is nil


原因:
* auto-config.lua脚本问题。当参数值等于false时,parse_value函数返回false,导致"if not val then"判断出错

解决:
* 修改lib/mysql-proxy/lua/proxy/auto-config.lua,handle函数代码

if not val then

改为
if val == nil then

* 然后重启MySQL Proxy服务

===proxy save config 保存配置文件大小等于0===
现象:
* proxy save config 命令保存的配置文件,文件大小等于0,要等待一段时间,或重启MySQL Proxy后,其大小才不等于0
-rw-r-----  1 oracle oinstall    0 Jul  4 15:36 "a.log"


原因:
* auto-config.lua脚本问题。文件写操作之后,没有刷新缓冲区

解决:
* 修改lib/mysql-proxy/lua/proxy/auto-config.lua,save函数代码

file:write(content)

之后,增加
file:flush(content)


===proxy save config 保存配置文件名带双引号===
现象:
* proxy save config 命令保存的配置文件,文件名包括双引号
-rw-r----- 1 oracle oinstall  186 Jul  4 15:59 "a"


原因:
* auto-config.lua脚本问题。文件名匹配包括双引号

解决:
* 修改lib/mysql-proxy/lua/proxy/auto-config.lua代码

local string_quoted  = l.P("\"") * ( 1 - l.P("\"") )^0 * l.P("\"") -- /".*"/

改成
local string_quoted  = l.P("\"") * l.C(( 1 - l.P("\"") )^0) * l.P("\"") -- /".*"/


                l.Ct( l.Cc("string") * l.C(string_quoted) +
...
...
         (SAVE / "SAVE" * CONFIG * WS^1 * INTO * l.C(string_quoted)) +
         (LOAD / "LOAD" * CONFIG * WS^1 * FROM * l.C(string_quoted))) * -1)

改成
                l.Ct( l.Cc("string") * string_quoted +
...
...
         (SAVE / "SAVE" * CONFIG * WS^1 * INTO * string_quoted) +
         (LOAD / "LOAD" * CONFIG * WS^1 * FROM * string_quoted)) * -1)



-fin-

1 comment:

Anonymous said...

感谢你的注释,对我理解rw这个脚本帮助挺大, 对于mysql 5.5与mysql proxy结合问题,我找到一个解决办法. 灵感来源于:http://dev.mysql.com/doc/refman/5.0/en/mysql-proxy-scripting-read-auth.html, rw-splitting.lua的具体修改法需要的话可以和我联系: queyimeng@gmail.com

Website Analytics

Followers