jinhailang / blog Goto Github PK
View Code? Open in Web Editor NEW技术博客:知其然,知其所以然
Home Page: https://github.com/jinhailang/blog/issues
技术博客:知其然,知其所以然
Home Page: https://github.com/jinhailang/blog/issues
Nginx 服务端很多时候需要知道请求客户端的真实 IP,但是实际请求客户端可能经过了很多层代理才到的服务端,而误将代理 IP 当作请求客户端的 IP 可能导致很严重的问题,比如在 IP 维度做请求限速或者请求统计时。
这种情况下,要怎样获取请求端真实 IP 呢?
X-Forwarded-For
请求头X-Real-IP
请求头这两种方式的原理其实是一样的,都是跟上下游代理之间约定某个特定字段,将请求 IP 放进去,然后层层传递下去。
X-Forwarded-For
一般包含了整个请求经过的所有对端IP,以逗号+空格分割,最前(左)端的 IP 就是第一个请求客户端的 IP,因为这种方式被广泛的使用,可以作为业界共识。
但是,很明显的是,这种约定是脆弱的,也不安全,客户端甚至可以在请求的时候自己指定 X-Forwarded-For
头,这样就相当于可以随意篡改自己的 IP 地址了。当然,一般我们真正关心的是请求进入公司内网时的 IP,所以,可以在内网最前面的代理上做过滤。
proxy_bind
允许代理指定发起请求的 IP,因此我们可以如下设置(Nginx 1.11.0 开始支持的):
proxy_bind $remote_addr transparent;
注意,需要开启 root
权限 user root;
。这样设置后,Nginx 后面的服务器看到的 IP($remote_addr
) 不再是 Nginx 反向代理服务器主机的 IP,而是真正发起请求的主机 IP。这就是所谓的“透明代理”。实现原理是在Linux 2.6.24以后,socket 增加了一个选项IP_TRANSPARENT可以接受目的地址没有配置的数据包,也可以发送原地址不是本地地址的数据包。可以通过该特性实现4层以上的透明代理。,详细分析可以看这里。
相对上面两种,这种方式相对更简便,也更安全(初看以为也能篡改 IP,其实是不行的,必须是发起请求的 IP 才行,原因自行思考一分钟吧)。
因此,最好都设置透明代理,这样后端就不需要特别去关注怎么获取真实的请求 IP 地址,直接取 ngx.var.remote_addr
值就是了。关键这也是最安全的方式,没有被篡改的风险。
想起以前做爬虫的时候,最大的难题就是爬取次数太多,IP 被限制了,只能花钱去买代理,然后代理又被封了,让人很头大。现在看来,也许可以在请求的时候加上 X-Forwarded-For
头?有时间可以试一试,理论上应该有些效果。
什么是规则引擎?简单说就是解释,执行规则的一类程序,本质上就是规则的编译器。
我们知道,编程语言都是由一系列的规范组成的,编译器根据语言规范,生成最终的机器码,因此,规则引擎跟程序员经常打交道的语言编译器并没有本质区别。但是,规则引擎定义的规则,都比较简单,没有编程语言那么复杂,规则引擎主要是分析,执行两步,一般没有复杂的代码生成(中间码或机器码)与优化模块。
前面说过,编程语言其实就是规则的定义,不同的是规则引擎的规则定义非常简单,主要包括两部分:条件和行为,可以很形象的用伪代码表示:
when
condition
then
action
规则条件的判断就是模式匹配过程,模式匹配是将数据源(也称 fact
)与条件匹配,这里的数据源不仅仅是指输入规则引擎的消息数据,还包括规则引擎内定义,实现的供规则调用的函数或数据变量。所以,规则引擎主要包括三类对象:
目前比较常用的规则引擎开源框架是使用 Java 开发的 drools 。
规则引擎的实现方案有以下几种:
就是直接将用户的规则写死到系统代码内,实现简单,但是难以维护,且很浪费程序员精力。
使用规则模板(一般是 Json 格式),后端按模板解析,执行。关键前期是模板的定义,实现相对简单,可读性强,符合普通用户的认知方式,但是,扩展性差,不够灵活,对于交复杂的场景,模板会变的很复杂且冗余,后端的模板解析代码同样会变的复杂且难以维护。
在编程语言中表达式,表达式包括操作数(常量,变量,函数等)和操作符(运算符等)。
通常情况下,用户的配置的业务规则其实就可以看作是一个表达式,表达式能够满足大多数业务场景,操作数据的库的 SQL 语句就是属于表达式的实现方式。
表达式足够灵活,相对比较安全,对用户学习成本较低,是项目成熟后比较常用的实现方案,后面也将重点讨论基于表达式的具体实现。
对于某些非常复杂的场景,需要支持用户直接定义函数,声明变量等,规则就相当于是代码了。前面提到的开源框架 drools
就支持 Java 代码的,广义上来说,类似 OpenResty 或游戏引擎这种支持运行脚本语言的系统,都可以看作是这种方案的实践。
自然的,这种方案几乎是没有边界的了,可以支持任何规则。对于通用框架来说,是有必要的,但是,对应特定的业务场景下,这种方式会带来多种弊端,需要用户具有一定的编程能力,学习成本较高,也提高了系统复杂度,不易维护,也不安全。
这里的编程语言应该就是主流的语言,而不是自己造轮子,发明一门新语言,给自己挖坑。
表达式是组成编程语言的数据结构之一,对于规则引擎来说用户输入的规则就是普通的字符串,规则引擎首先要对表达式字符串进行解析,解析的过程包括两个步骤,也即编译原理中的词法分析
与语法分析
,最后生成抽象语法树
,这里就是表达式的语法数,最后递归遍历执行。
词法分析(英语:lexical analysis)是计算机科学中将字符序列转换为标记(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。
简单来说,就是对代码字符串的字符进行分类,比如空格,关键词等。
语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
Go 的 parser 接受的输入是源文件(字符串),内嵌了一个 scanner,scanner 生成的 token 经过语法分析,生成 AST 返回。
Go AST 主要有三类节点组成:表达式,语句,声明。Go 语言规范 详细的定义了这三类数据结构,以及所子类。需要说明的是,这里的表达式,包括了 Types(类型)和 primary expressions(基本表达式)。
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
基于 Go 基本编译方法,递归遍历执行 Go ParseExpr 函数返回的表达式 AST,实现基于表达式的规则引擎。在我的开源项目 https://github.com/jinhailang/gre 有详细的文档说明跟代码实现,这里不再复述了。
规则引擎应用非常广泛,在用户操作比较复杂的场景,经常被用到。例如:风控系统,Waf 系统等安全类系统,游戏引擎,数据库等通用技术框架。
规则引擎应用于较为复杂的业务场景,增强了系统的扩展性,降低系统耦合,很大程度的提高了开发人员的工作效率,满足用户多种需求,使开发者和用户都得到解放。
规则引擎的核心是规则解析,基础是数据源。规则的解析依赖 Json 结构(模板配置)或 AST,AST 是由语法分析器生成,对于很多编程语言,都直接暴露了 API 调用(比如 Go 就开放了很丰富的编译器实现 API),绝大多数情况,都没有必要自己造轮子。
数据源包括外部输入的消息数据,以及规则引擎内实现的方法或定义的变量。规则的执行是基于数据源的,因此,规则引擎开发之前,首先要明确数据源,也即明确外部需求。
用户反馈说线上程序貌似没有更新,承诺的新特性都没有。去线上确认,发现程序内存版本号确实没有更新。由于项目是使用 shell 脚本命令(docker kill --signal "HUP" xxx
)重启的,当时就怀疑是不是重启命令没有被正确执行?但是查询日志,发现更新时,有 init_by_lua
阶段的日志输出,就是说 重启指令执行了,且 init_by_lua
阶段正常被执行了,但进程内的代码数据还都是旧的,而且使用 nginx stop
重启更新是正常的,这就很诡异了!
因为之前版本没有发现过,只能猜测是新版代码有问题,但是,新版代码都是些业务代码之类的更新,怎么会出现这种问题呢?很让人抓狂,只得老老实实在测试环境,模拟一下新旧版本的切换场景,没有想到,出现了下面的错误:
会不会线上也是因为端口占用,导致的 reload 失败呢?去线上再次查日志,发现更新的时间点,果然也有端口占用的错误日志(/var/log/syslog
):
时间上刚好与 init_by_lua
阶段输出的日志时间一致。
这里脚本执行也有个问题,由于 reload 后没有正确判断执行结果,导致只能去 /var/log/syslog
下查日志才发现。
基本可以确认是因为执行 Nginx reload
时,出现了端口被占用的错误,导致更新没有完成。而 init_by_lua
被执行的是因为该阶段在端口监听之前被执行(从上面日志可以直接看到)。
所以,到底是什么操作触发了这个错误呢?既然是端口监听的问题,自然跟 nginx 的 listen 配置有关,联想到之前,为了安全起见,**将配置 listen 8081
改成了 listen 127.0.0.1:80
。**于是,在本地尝试复现,发现每次必现:
至此,问题原因基本查清楚了,就是地址端口修改导致的“血案”,而且只有在 listen 8081
和 listen x.x.x.x:8081
格式之间切换时,才会出现这种问题。
既然是 listen 配置问题,那么直接使用固定 listen IP:PORT
就可以避免了,而且很重要的是,listen PORT
这种用法也是很不安全(可能被外网访问),且不专业的。
为了找到百分百的实锤,也是为了彻底搞清楚 reload
的详细过程,查看了下这块 Nginx 代码。与热(Nginx 二进制)更新(kill -USR2
) 相比,reload 实现代码相对简单些。
Nginx master 进程收到 HUP
信号后将 ngx_reconfigure = 1
,会执行函数 ngx_master_process_cycle
内的以下代码块:
if (ngx_reconfigure) {
ngx_reconfigure = 0;
if (ngx_new_binary) {
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
ngx_noaccepting = 0;
continue;
}
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_core_module);
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_JUST_RESPAWN);
ngx_start_cache_manager_processes(cycle, 1);
/* allow new processes to start */
ngx_msleep(100);
live = 1;
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
这里关键函数是 ngx_init_cycle,这个函数负责解析初始化配置,并创建新的 ngx_cycle_t 结构体以及清理旧的 ngx_cycle_t 。
进入 函数 ngx_init_cycle(ngx_cycle_t *old_cycle)
,与监听相关的片段有两处(按前后顺序):
以上代码说明,reload 后首先会遍历旧结构体内原来的监听的句柄,如果监听的地址和端口都跟新的配置结构体相同,则直接将句柄添加到新的结构体。然后才会关闭旧结构体内没有被新结构体引用的监听句柄。
ngx_cmp_sockaddr
函数负责判断新旧配置监听的地址端口是否相同,主要实现如下:
if (cmp_port && sin1->sin_port != sin2->sin_port) {
return NGX_DECLINED;
}
if (sin1->sin_addr.s_addr != sin2->sin_addr.s_addr) {
return NGX_DECLINED;
}
经过源码分析,就很清晰自然了,由于 listen 8081
监听的实际上是 0.0.0.0:8081
,改成 127.0.0.1:8081
后,reload 时会先重新创建监听 127.0.0.1:8081
的句柄,自然会与原来的 0.0.0.0:8081
监听冲突了。Nginx 这么设计是很合理的,是配置使用的问题,线上不应该使用 listen 8081
这种指令,显然很不安全。
最后,梳理下 reload 基本过程:
HUP
信号ngx_init_cycle
函数,创建新的结构体 ngx_cycle_t
,解析加载配置。监听配置的地址端口(直接引用或新创建)此时,有两种情况:
如果 ngx_init_cycle
执行失败,则还是会使用原有的配置(回滚)。
如果 ngx_init_cycle
执行成功,则使用新的配置(ngx_cycle_t
结构体),新建所有 worker 进程。新建成功后发送退出信号给旧的 worker 进程。
旧 worker 进程接收到到退出信号后会继续服务,当所有请求的客户端被服务后,worker 进程退出。
end:)
后面准备开发内部的数据API平台,因为目前公司用 redis
比较多,为了能够平滑的迁移,所以,对 JOSN
的处理很重要,调研试用了MongoDB,MySQL 和 PostgreSQL,大概比较了一下,比较倾向于 PostgreSQL,主要原因如下:
某个程序因为系统内存不被杀掉了,此时,由于没有输出详细的堆栈信息,很难定位问题。
可以设置 overcommit_memory
参数,禁止系统启用 overcommit
机制(原理参考上面链接的文章)。
echo 2 > /proc/sys/vm/overcommit_memory
这样当系统内存不足时,程序就会主动 panic
从而输出 stack trace
(以 Go 为例)。
Nginx 在 postconfiguration 阶段执行 lua-nginx-module
模块初始化函数 ngx_http_lua_init
, 该函数会调用ngx_http_lua_init_vm
来创建和初始化一个 lua 虚拟机环境,由 lua API luaL_newstate
实现,该接口函数会创建一个协程作为主协程,返回 lua_state(存放堆栈信息,包括后续的请求数据(ngx_http_request_t
),API 注册表等数据都是存放在这里,供 lua 层使用),然后调用 ngx_http_lua_init_globals
,该函数做了两件事:
global_state
数据结构,这个结构体保存全局相关的一些信息,主要是所有需要垃圾回收的对象。ngx_http_lua_inject_ngx_api
,注册各种 Nginx 层面的 API 函数,设置字符串 ngx
为表名,lua 代码中就可以使用 ngx.*
来调用这些 API 了。另外,还会调用函数 ngx_http_lua_init_registry
, ngx_http_lua_ctx_tables
就是在这里注册到 Nginx 内存的(lua 中没有引用的变量会被 GC 掉),用来存放单个请求的 ctx 数据(table),即 ngx.ctx
。所以,与 ngx.var
不一样,ngx.ctx
其实是 lua table,只是在 Nginx 内存中添加了引用。也就不难理解,ngx.ctx
生命周期是在单个 location,因为内部跳转时,会清除对应的 ctx table。要想在父子请求间共享 ngx.ctx,可以参考这篇文章,过程大概是,将对应的 ctx 再次插入 ngx_http_lua_ctx_tables,创建新的索引,索引保存在 ngx.var 中,在子请求时取出重新赋值给 ngx.ctx。
master fork worker 进程时,Lua 虚拟机自然也被复制(COW)到了 worker 进程。
请求是在 worker 进程内处理的,处理共分为 11 个阶段,其中在 balancer_by_lua, header_filter, body_filter, log 阶段中,直接在主协程中执行代码,而在 rewrite_by_lua, access_by_lua 和 content_by_lua 阶段中,会创建一个新的协程(boilerplate "light thread" are also called "entry threads")去执行此阶段的 lua 代码。这些新的子协程相互独立,数据隔离,但是共享 global_state
。
为什么 content 等几个阶段的处理要在子协程里面处理呢?原因可能是 content 等阶段,需要调用 ngx.sleep
,ngx.socket
I/O 之类的阻塞操作,使用协程实现异步,提高执行效率。如果放在主协程,这类操作就会阻塞主协程,导致 worker 进程无法处理其它请求。ngx.socket
,ngx.sleep
等 API 都会有挂起协程的操作,只能在子协程调用,因此,这些 API 不能在 header_filter 等阶段(主协程)使用。
我们知道,协程是非抢占式的,也就是说只有正在运行的协程只有在显式调用 yield 函数后才会被挂起,因此,同一时间内,只有一个协程在处理(因为 worker 是单线程的),lua 协程还有一个特性,就是子协程优先运行,只有当子协程都被挂起或运行结束才会继续运行父协程。
ngx_lua 协程的调度可以参考下面这张图(图片来自):
lua_resume
就是恢复对应的协程运行,在请求处理时,还可能调用 API ngx.thread
来创建 light thread
, 可以认为是一种特殊的 lua 协程,没有本质区别,不同的是,它是由 ngx_lua
模块进行调度的(详见下面的 ngx_http_lua_run_thread
源码)。在需要访问第三方服务时,并发执行,可以减少等待处理时间。
ngx.thread.spawn(query_mysql) -- create thread 1
ngx.thread.spawn(query_memcached) -- create thread 2
ngx.thread.spawn(query_http) -- create thread 3
从上面可知,在 ngx_lua 内有三层协程 —— 全局的主协程,请求阶段的子协程,以及用户创建的 light thread
,它们分别为父子关系,记住这三个协程代称,后面将会用到。
使用 ngx.exit
, ngx.exec
, ngx.redirect
可以直接跳出协程,而不用等待子协程处理完成。
ngx.exec("/a/b/c") -- 内部跳转,直接从子协程结束,回到主协程
ngx.redirect("/foo", 301) -- 重定向,终止的当前请求的处理,即不再处理后续阶段
ngx.exit 可接受多种参数:
ngx.exit(ngx.OK) -- 完成当前阶段(退出子协程),继续下一个阶段
ngx.exit(ngx.ERROR) -- 中断当前请求,报错
ngx.exit(HTTP_STATUS) -- 结束 content 阶段,继续下个阶段
返回值说明
light thread
)被挂起light thread
都结束)以上,就是协程在 ngx_lua
模块中的使用与调度。那么 lua 协程到底是个什么神奇的东西呢?
Lua 所支持的协程全称被称作协同式多线程(collaborative multithreading),由用户(lua 虚拟机)自己负责管理,在线程内运行,操作系统是感知不到的。特性就如上面所说,主要两条:
是不是很像回调?因此,lua 协程之间不存在资源竞争,也就不需要锁了。严格来说,这种协程只是为了实现异步,而不是并发。而且,lua 是没有线程概念的,lua 语言的定位就是系统嵌入式脚本,由 C 语言调度使用的,在 C 层面创建线程就行了,也使得 lua 更加简单。
主要 API
yield
的参数值(b1 ... bn)resume
的参数(a1 ... an)有趣的实例
使用 lua 协程实现生产者-消费者问题:
local i = 0
function receive(prod)
i = i + 1
local status, value = coroutine.resume(prod, i)
return value
end
function send(x)
return coroutine.yield(x)
end
function producer()
return coroutine.create(function()
while true do
local x = io.read()
local r = send(x)
io.write(i,":\r\n")
end
end)
end
function consumer(prod)
while true do
local obtain = receive(prod)
if obtain then
io.write(obtain, "\n\n")
else
break
end
end
end
io.write(i+1, ":\r\n")
p = producer()
consumer(p)
从这里可以看到,lua 协程跟线程差别很大,更像是回调,new_thread 只是在内存新建了一个 stack 用于存放新 coroutine 的变量,也称作lua_State
。
lua 协程与 golang 协程区别
lua 协程与 golang [协程都是协程,但是差别还是挺大的,除了都有自己独立的堆栈空间外,唯一的共同点可能就是前面说过的都是非抢占式的(实际上[Go 1.2 开始加入了简单的抢占式调度逻辑](https://golang.org/doc/go1.2#preemption))。一个最明显的区别是,golang 父子协程是独立而平等的。
golang 调度器实现更复杂,可以将协程分配到多个线程上(GPM 模型),因此,golang 协程是可以并发(并行)的。本质上,lua 协程主要作用是单线程内实现异步非阻塞执行;golang 协程与线程更加类似,用来实现多线程并发执行。
OpenResty 将 lua 嵌入到 Nginx 系统,使 Nginx 拥有了 lua 的能力,大大的扩展了 Nginx 系统的开发灵活性和开发效率。达到了以同步的方式写代码,实现异步功能的效果。不用担心异步开发中的顺序问题,又因为单线程的,也不用担心并发开发中最头痛的竞争问题。比起原生的 Nginx 第三方模块开发,开发更简单,系统也更稳定。
需要注意的是,ngx_lua 并没有提高 Nginx 的并发能力,Nginx worker 本来就是使用回调机制来异步处理多个请求的, 当前请求处理阻塞时,会注册一个事件,然后去处理新的请求,从而避免进程因为某个请求阻塞而干等着(参考知乎问答)。
unsafe
包的作用有两个:
包接口比较简单,包括 3 个函数:
unsafe 函数都是在编译时计算返回结果的,所以,可以直接用于常量赋值,也要注意,尽量不要将运行时变量类型(例如 slice)作为这些函数的参数出入,可能会导致非预期的结果。
包括 2 种类型:
unsafe.Pointer
是本包的精华,也是被使用最多的功能点。Pointer 允许程序(开发者)跳脱 Go 的类型系统,(通过指针转换与运算)读写任意内存,所以要小心使用。
Pointer 总结下来就两个特性,也是实现前面说的目标(作用)的基础:
uintptr
是无符号整型,被用来存放指针值(地址)。unsafe.Pointer
+ uintptr
就能实现指针偏移计算了。因为 uintptr
变量存放的是某个变量的地址,因此,uintpter
变量值对应的内存地址块(对应的变量)可能会被 GC 回收掉。unsafe.Pointer
本质上就是指针,该类型变量指向的内存块则不会被回收,因此,应该使用 unsafe.Pointer
类型变量来保持变量地址不被回收。
// 不安全的使用
z := uintptr(unsafe.Pointer(&xx))
//todo ...
fmt.Println(z)
//正确使用
sp:=safe.Pointer(&xx)
z = uintptr(sp)
//todo ...
fmt.Println(z)
前面说过,使用 Pointer 必须要非常小心才行,官方定义了 6 种安全有效的使用场景。使用 go vet
工具可以检测出不符合这些场景的调用。
*T1
转化成 *T2
如果 T2 大于 T1 变量类型的内存占用,并且两者共享等效的内存布局,则该转换允许将一种类型数据解释成为另一种类型。例如:int64 与 float64。
与 Pointer 不同,uintptr 保存的地址指向的变量是可以被 GC 回收的。
可以使用 runtime.KeepAlive
函数避免变量被 GC。
通常用于访问结构体或者数组等:
// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))
特别注意:因为上面说的原因,uintptr 不能放在临时变量内,所以下面这样分开使用也是无效的:
u := uintptr(p)
p = unsafe.Pointer(u + offset)
另外,做地址偏移的时候,要注意越界的问题,比如:
a = []int{0,1,2,3}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + len(a) * unsafe.Sizeif(a[0]))
此时,变量 p 指向的地址是未知的,可能会出现不声明,直接偷偷的读写未知内存地址的情况,对系统运行稳定性影响很大。
syscall.Syscall
时,将 Pointer 值转成 uintptrsyscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
Data
字段返回的也是 uintptr:
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
hdr.Len = n
reflect.SliceHeader
的使用:
package main
import "fmt"
import "unsafe"
import "reflect"
import "runtime"
func main() {
bs := []byte("Golang")
var pa *[2]byte // an array pointer
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
pa = (*[2]byte)(unsafe.Pointer(hdr.Data))
runtime.KeepAlive(&bs)
fmt.Printf("%s\n", pa) // &Go
pa[1] = 'a'
fmt.Printf("%s\n", bs) // Galang
}
如果最后一行的 Printf 不存在的话,runtime.KeepAlive
的调用是必须的。
另外,最好不要像下面这样,直接从 StringHeader 或 StringHeader 直接创建对象:
// Assume p points to a sequence of byte and
// n is the number of bytes in the sequence.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// Now the just allocated byte array has lose all
// references and it can be garbage collected now.
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr))
在有些场景下,使用 unsafe.Poniter
可以帮助我们写出高效的代码,例如在 sync/atomic
包内的使用。而且一些底层或 C 调用,必须要用到 Poniter。
unsafe 包用于有经验的开发者绕过 Go 类型系统的安全性限制,一定要深入理解上面的六种使用场景,谨慎使用,否则很容易引起严重的内存问题,有经验的开发者都知道,这类问题通常是很难定位的,对系统的稳定性影响很大。
锁是在执行多线程时用于强行限制资源访问的同步机制,在分布式系统场景下,为了使多个进程(实例)对共享资源的读写同步,保证数据的最终一致性,而引入了分布式锁。
分布式锁应具备以下特点:
可以基于数据库,缓存,中间件实现分布式锁,比较主流的是使用 Redis 或 Etcd (java 可能更多的是用 ZooKeeper) 来实现,当然也可以基于数据库等支持事务的中间件实现,但相对不够健壮,也不够安全,一般不推荐,这里就不展开说明了。结合以上的四个特点,下面将深入讨论这两种方案的实现方式与原理。
Ectd 是一个高可用的键值存储系统,具体以下特点:
重要的是,etcd 支持以下功能,正是依赖这些功能来实现分布式锁的:
实现过程
就实现过程来说,跟“买房摇号”很相似。
1、定义一个 key 目录(如:/xxx/lock/
)用于存放客户端(进程)的操作 ID。类似申请买房的号码牌;
2、客户端先 put
key /xxx/lock/id
,id 是全局唯一的,可以使用 UUID,并设置过期时间 TTL
,防止死锁。记下返回的 Revision
值 R
。类似你拿到一个选房序号,并规定了进去选房时间,超时还没有选中,就失效了;
3、get
目录 /xxx/lock/
下所有的 key 及对应的 Revision
值,与上一步返回的 Revision
值进行比较:
Revision
值 R 小于或等于目录下所有的 key 对应的 Revision
,则当前客户端获取到了锁。类似你是排在第一个选房的,不用等了,直接选房就是;/xxx/lock/
。盯紧大屏幕,等待排你前面的人选房;4、当所有靠前的 key 都被删除之后,则意味着的客户端获取到了锁。类似前面的人都选好房或者弃权了,终于轮到你选房了!
但是,这里有两个问题,也是分布式锁实现方案之间的重要区别:
Etcd 和 Redis 给出了不同的答案,后面将会对比阐述。
Redis 可以使用 SET 命令:
SET KEY VALUE NX PX 100
这里的 KEY 是同一个,VALUE 最好是全局唯一的(原因后面会知道),如果执行成功,则意味着获取到了锁;如果失败则循环尝试,类似自旋锁的获取过程,但这里不需要太频繁,可以 Sleep
一段时间,还可以对续约次数进行限制。
看起来,这个实现方案比 Etcd 实现要简单很多,区别就是,Etcd 实现的是公平锁。但是,结合上面提的两个问题,就会发现,这只是一个简单的实现,并没有给出问题的答案。
对于那两个问题,Etcd 与 Redis 给出了不同的答案。
1)问题一,租约(比工作完成时间)提前到期的问题。
Etcd
本身支持 KeepAlive
机制,来进行租约续期,在 put
操作成功之后,对 KEY 设置 KeepAlive 即可。Etcd 的租约是与 KV 单独分开的,有自己的租约 ID,所以实现起来并不复杂。
Redis
Redis 本身没有 KeepAlive
的机制,所以,只能客户端自己模拟实现:
1、首先客户端 SET 时,VALUE 要是全局唯一的,也可以使用 UUID,并记下这个 VALUE 值;
2、使用单独的线(协程)程 GET KEY,并对比 VALUE 值是否与前面的记录的值相同,如果相同,说明当前客户端仍然持有锁,通过 EXPIRE
更新 KEY 失效时间;
3、当工作完程,释放锁(删除 KEY)之前,先关闭这个续约线程,并且删除 KEY 之前也要比较 VALUE 是否与本客户端设置的一样,防止释放别的客户端持有的锁;
两种续约方式,基本原理,效果都类似,Etcd 更优雅一些。
2)问题二,保证节点数据一致性的问题。
这是分布式架构中的基础也是经典问题,一般分布式系统中为了保证分区容错性,节点(数据)都是主备的。
对 etcd 主节点写入时,要保证所有主从节点都写入成功,才会返回写入完成,也即是主从同步复制,这样就可以保证主从节点的数据强一致性。Redis 由于历史原因,刚开始都是单机部署的,后面才支持集群部署,为了保证性能,主从使用异步复制,因此,并不保证节点间数据的强一致性。
Redis 集群一般有多个 Master 节点,数据负载到不同的 Master 节点上(数据分片)。这种场景下,实现分布式锁时更加麻烦,因为,为了保证当前只会出现一把锁,就必须要设置 KV 到所有 Master 节点才行(实际只要超过一半就行)。为了解决这个问题,Redis 作者基于 Redis 设计实现了 Redlock 算法,实现过程过程如下:
1、得到当前的时间,微妙单位。
2、尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间。
3、当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
4、如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间。
5、如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态。
当然,也可以取个巧,客户端约定在同一个 Master 申请/释放 锁,但是,这样客户端处理起来又太累赘了,不够通用。
通过深入分析分布式锁的实现,可以发现,由于 Redis 主要是用于数据读写缓存,需要优先保证大流量场景下读写性能,分区容错性以及服务可用性是最重要的;而 Etcd 主要用于配置分发,必须要保证数据强一致性以及分区容错性。
这也就是 CAP 理论实例,根据系统应用场景来做取舍,选择最合适的实现方案。对于分布式锁的使用场景来说,使用 Etcd 来实现分布式锁,要更加的简洁,也更加安全。
虽然 Etcd (V3) 官方已经支持了分布式锁的 API 实现,为了理解的更深刻,我自己也造了个轮子https://github.com/jinhailang/rainforest/tree/master/ivy。此外,因为支持事务(基于软件事务内存机制(STM)实现),所以,还可以使用事务来实现分布式锁,具体参考NewSTM 使用实例。
PS: 事务也是非常常见而且非常重要的概念,我也在文章 #48 较详细的阐述了事务的应用及原理。
keepalive_requests
踩坑总结Waf
(基于 Nginx 实现的七层防护系统)上线上后,QA 同事对系统进行大流量压测(十万级 QPS),发现部署机器处于 TIME_WAIT
状态的 TCP 连接数异常,高峰期达到几千。按理 waf 这边使用的是长连接,连接数不应该这么多的。
首先确认下 TCP 连接处于 TIME_WAIT 原因:
TCP 连接主动关闭方会处于较长的 TIME_WAIT 状态, 以确保数据传输完毕, 时间可长达 2 * MSL, 即就是一个数据包在网络中往返一次的最长时间。
其目的是避免连接串用导致无法区分新旧连接。
这就说明确实是服务端这边主动关闭了连接。我重新 review 了一下 Nginx 配置,确实使用了长连接,并且,我上线前在本地使用 ab 工具压测过, 没有观察到连接数量异常情况。
只能祭出 google 大法了,发现可能是 keepalive_requests 设置太小引起的。
keepalive_requests 参数限制了一个 HTTP 长连接最多可以处理完成的最大请求数, 默认是 100。当连接处理完成的请求数达到最大请求数后,将关闭连接。
而我并没有配置 keepalive_requests
,所以,就是使用的默认数 100,即一个长连接只能处理一百个请求,然后 Nginx 就就会主动关闭连接,使大量连接处于 TIME_WAIT
状态。
我猜测很大可能就是这个原因了,为了在本地验证,我将 keepalive_requests
分别设为 10
, 50
, 100
, 1000
,分四组分别进行压测,对比结果。
压测命令:
ab -n100000 -c10 -k http://127.0.0.1:8086/test
然后使用 netstat -nat |awk '{print $6}'|sort|uniq -c
查看连接数,结果出乎意料的清晰,TIME_WAIT
数量基本等于 总请求数/keepalive_requests,这就基本证实了猜测。
其实大部分情况下,使用默认值是没有问题的,但是对于 QPS 较大的情况,默认数值就不够了,综合考虑,在 Nginx conf 增加配置项:
keepalive_requests 1024;
可能很多人更习惯不做限制,可以设置一个最大的数来实现这个效果 -- 2^32 - 1 = 4294967295
(这是在运行32位和64位系统的计算机中是无符号长整形数所能表示的最大值,亦是运行32位系统的计算机中所能表示的最大自然数),
虽然问题解决了,但是,为什么 Nginx 要对单个长连接的处理请求数进行最大限制呢?一般我们理解的,长连接是在连接池里维护的,只要连接本身没有超时等异常问题,是可以一直复用的。
关于这个疑问,讨论的似乎不多,只在 Nginx 开发者论坛上看[nginx] Upstream keepalive: keepalive_requests directive.有所讨论,开发者有模糊的表述:
Much like keepalive_requests for client connections, this is mostly
a safeguard to make sure connections are closed periodically and the
memory allocated from the connection pool is freed.
但是,为什么需要定时关闭连接,释放内存?难道这只是一种保护策略,怕极端情况下,用户配置不当,导致连接一直不断开,引起内存问题?很奇怪,从 Nginx 开发日志也没有找到特别说明。
另外,上面的讨论贴其实是讨论 Upstream keepalive_requests 设置的,最新版 Nginx (version 1.15.3.) 在 Upstream 内增加了 keepalive_requests
配置项,与 keepalive_requests client connections
类似,默认值也是 100。也就是说,之前的版本,连接上游的连接是没有次数限制的,完全由上游的服务端的 keepalive_requests
设置,但是,如果上游服务端不使用 Nginx 的话,别的系统一般也是没有这个限制的。
在大流量的场景下,Nginx 代理升级到这个版本,就可能会引起连接异常情况。关于默认值和兼容性,作者的回复也比较主观,可能这块确实很难面面俱到吧,只能满足大多数场景了。
综上,在 QPS 较高的场景下,服务端要注意将 keepalive_requests 设置的大一些,如果 Nginx 升级到最新版,还需要注意设置 upstream 的keepalive_requests,这两个数量可以不一致,一般服务端要设置的大些,因为一般来说,一个服务端可能对应多个代理。
todo
闭包原本就是指所有的函数,但我们一般是指能够读取其他函数内部变量的函数,主要有两个作用:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。匿名函数不但可以省去命名的问题,同时可以提高程序的安全性,增加内聚。this 指针的作用对象取决与其所在的运行环境,闭包是运行在 Window 全局的,所以闭包里面的 this 也是指向 Window 的。在 JavaScript 中一切都可以看见作对象,变量,函数等都可以用来创建对象。在 JS 中属性是公有的,但是有私有变量,使用 var 定义就是私有变量,同时也没有块级作用域的概念(注:for(var i=0;i<5;i++),i存在于整个函数),那怎么才能访问私有变量以及仿造块级作用域,跟其他语言一样的效果呢?
闭包+匿名函数就派上用场了。块级作用域可以使用自我执行来解决(注:function(){}()),为什么?这里要引入作用域链和内存回收的概念了。JS 中对象之间的关系是从下往上的,也就是说如果子函数还在引用,那么以上的所有父函数都不会被回收,所以上面的 for 循环就可以放在函数闭包里面自我执行,完了后,i就被回收了,相当于块级变量。
既然通过闭包等方式可以创建出跟其他语言一样的对象,那么也肯定可以达到一些特殊的模式设计效果了。比如,静态变量,单例模式。使用 prototype 使方法(如构造函数等)共享,从而使相应的变量变成静态变量。
JS 相对于 C++ 而言更加面向对象(貌似是个废话),两个语言的所有用法不同之处就在于此,JS 中的许多特性就是强制要使用者养成面向对象的概念,在语言层面很自然的能够设计出高内聚,低耦合的程序来。比较适合初学者,但 C++ 更加自由化一些,要有一定的功力才能达到这样的效果。
io -> io/ioutil
bufio
net/http
实现原理以及之间的应用关系?
Go 引用的包,可以分为两类:内部包和外部包,对于内部包,因为是项目内部自己写的,管理起来比较简单;但是,对于外部包,因为是开源的,一般由第三方维护管理,可能会版本不一致,出现非预期的情况,导致编译错误,或者出现程序 BUG,有经验的程序员应该都知道,这种由于第三方包引起的问题,定位和解决往往都是很烦人的。所以,目前所谓的依赖包管理工具,都是针对外部包的管理。
大部分编程语言都是将项目代码目录作为单独的工作目录(workspace),但 Go 只有一个工作目录 ——
$GOPATH,因为 Go 自身提供了很多工具,固定的工作目录,便于这些工具的运行。因此,需要强调一点是,我们应该在 $GOPATH/src
下,创建自己的项目目录。因为,项目构建时,就是从这个路径下开始的(即作为环境路径)。
其实,对于 Go 本身来说,是不区分内部包和外部包的,因为 go get
下载的包,也都放在 $GOPATH/src
目录下,因此,当我们 import
依赖包时,只要直接路径指定就行了,类似 github.com/xx/x
。又因为 $GOPATH 目录下可能创建了多个项目,因此下载在这里的依赖包,也可能会被多个项目引用,随时可能会被其他项目更新修改。
所以,在最开始的时候(Go 1.5 之前),go 项目包的依赖管理是很简陋的,为了保证外部包的安全,就必须要把外部包拷贝到项目内部,变成“内部包”。此时,包的查找顺序是: $GOROOT --> $GOPATH.
在 Go 1.5 时,为了解决这个问题,引入了 vendor
这一特殊目录,该目录在项目内创建,这就相当于,每个项目有了自己独立的 GOPATH
,此时,包的查找顺序是:离引用代码最近的 vendor
--> $GOROOT --> $GOPATH.
项目内的每个目录下都能创建自己的 vendor
目录,引用时,使用的是最近一层的 vendor
。但是,最好只在根目录创建 vendor
,便于管理,也避免同样的包,出现在同一项目内的多个 vendor
目录下。
基于 vendor
能够实现真正意义的依赖包管理,而告别简陋的拷贝操作。
内部包管理其实很简单,但是有一个特性需要说明,那就是 internal
目录,位于 internal
目录下的包,只能被同父目录下的包引用,否则会编译报错。 这里需要说明的是,父目录的父目录也算,即位于项目根目录下 internal
里面的包,是可以被项目内所有包引用的。
与 vendor
类似,项目内也可以有多个 internal
目录,但是建议在项目根目录创建一个就够了。
internal
目录的目的是保护该目录下的包被其他项目所引用,例如某个包还不太成熟,不想被其他项目使用。很明显,这只是一种简单的“警告”机制。
基于 vendor
实现的依赖包管理工具,比较流行的有 dep
, Godep
, Glide
, Govendor
等,实现原理大同小异。官网这里对这几个工具进行了分类说明,建议使用 dep
作为项目的依赖管理工具,原因主要有两点:
dep
是官方推出的管理工具,虽然目前还是处于 official experiment.
状态,但是已经可以作为正式工具使用了;Godep
已经是 Gopher 比较常用的依赖工具了,但是可以看到,其官网已经声明后面只会进行维护性的开发工作,建议大家使用 dep
或其他工具代替:Please use dep or another tool instead.
dep 的使用,官方有较详细的说明文档:dep introduction.
这里根据自己的理解,做些说明。
安装
主要有两个安装方式:
$GOPATH/bin
目录下: mv dep-linux-amd64 $GOPATH/bin/dep
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
当然你也可以使用源码安装,但是,你最好不要这么干。安装完成后,执行 dep -h
查看帮助信息。
使用
dep init
dep init
cd $GOPATH/src/dep-test
dep init
完成后,在项目根目录下会创建文件 Gopkg.lock
, Gopkg.toml
和 vendor
目录,vendor
里面就是依赖的包源码了,前两个文件保存内保存了依赖包的规则类型信息,依赖包版本信息以及依赖关系描述,后面还会对 dep 实现原理进行简单说明。
dep ensure
自动下载项目所有引用的依赖包
dep ensure add
下载指定路径的引用包,当有新的包引用时,尽量使用这种方式,而不是偷懒直接用 dep ensure
,因为这个指令会将引用包版本信息自动写入文件 Gopkg.toml
,否则只会更新文件 Gopkg.lock
;dep ensure -update
更新依赖包版本,可以指定包路径,否则会更新所有依赖包;dep ensure -vendor-only
只会根据当前的 Gopkg.lock
文件内包信息下载依赖包,而不会根据项目引用,自动更新 Gopkg.lock
;dep ensure --no-vendor
会更新 Gopkg.lock
,但是不会更新 vendor
;dep check
检查 vendor
里面的包源码是否与 dep 记录的信息一致,如果不一致,则使用 dep ensure
更新;
实际应用中,提交代码时,我们可以将 vendor
目录一起提交,此时 其他开发者应该先使用 dep check
检验。
但是,一般 vendor
目录都比较大,这时可以只提交 Gopkg.toml
和 Gopkg.lock
文件,开发者在本地使用 dep ensure -vendor-only
自行下载依赖包。
实现模型也有详细说明:Models and Mechanisms,dep
使用 four state system
模型,这是一种经典的包管理模型。这四个模块分别是:
Project source code
当前项目代码,即你的项目代码A manifest
依赖清单,这里就是指 Gopkg.toml
文件,最初由 dep init
生成,主要由用户编辑,来实现个性化需求。它包含几种类型的规则声明来管理 dep 的行为,可以实现灵活的下载依赖包,例如可以指定特定区间版本的依赖包。A lock
依赖描述文件,这里是指 Gopkg.lock
文件,里面详细记录了依赖包的地址,hash 值,版本等基本信息,根据描述能够复现完整的依赖图。这个文件完全是由 dep
根据 Gopkg.toml
和项目引用自动生成的。Source code of the dependences themselves
依赖包自身源码,这是就是指 vendor
目录下的代码。dep
主要就是围绕对这四个模块进行输入输出管理实现。流程大概如下:
dep
根据项目引用代码和 Gopkg.toml
文件内的规则信息,计算自动生成 Gopkg.lock
文件,此时,依赖的包信息就确定了,然后根据这些信息将对应的包下载到 vendor
目录内。
使用软链接问题
对于 go 这种必须将项目放在 $GOPATH/src
的约定,实际应用中可能并不灵活,于是有两个技巧:
$GOPATH/src
下创建一个软链接,指向项目目录;但是,当使用软链接这种方式的时候,vendor
目录会失效,变成普通的目录了,因为 vendor
目录只有位于 $GOPATH
路径下,Go 才会把它当作特殊目录处理。而符号链接只是创建了指向文件名的符号,实际的文件位置依然没变。
使用相对路径问题
实际上对于内部包,我们在引用时可以使用相对路径(相对 import
代码的路径,例如: ./a/b
或者 ../x/y
等),这可能是 go 命令的潜规则,这种方式相对较灵活而且直观,项目存放位置也可以很随意。所以对于简单项目,个人是比较喜欢使用这种方式的。
但是,这会影响 dep
生成依赖关系,导致 dep
误以为没有依赖的外部包,非常奇怪的问题。
其实,这两个坑都是因为我之前比较随意,没有按规范,将项目放在 $GOPATH/src
导致的。
dep
还没有应用推广,在 Go 1.11 中,又推出了全新的依赖包管理机制 Go Modules
,原来叫 vgo
,在 Go 1.11 被采纳合并到了主分支,正式被发布,不出意外的话,后续版本应该都会将这个当作包依赖管理的官方解决方案。
模块为 GOPATH
提供了替代方案,用来为项目定位依赖和管理版本化。如果 go 命令在 $GOPATH/src
之外的目录中运行,并且该目录中有一个模块的话,那么模块功能就会启用,否则 go 将会使用 GOPATH(Go 1.11)。与 dep
不同,模块是集成在 go 命令的,可以使用 go mod init
创建。
Go 作为比较新的语言,包依赖管理方式也在持续变动,主要分为三个阶段: 纯依赖包源码拷贝 --> 基于 vendor --> Go Modules
。
每次变动都使管理变的更先进,这对于 Go 和 Gopher 都是有益的,特别是 Go Modules
据称使用了其它语言包管理工具不同的理念算法,看起来比较复杂。因为刚出来,有较多问题需要讨论,后续比较成熟了再研究,可能要等到 Go 1.12 版本吧。
所以,如果不想成为第一个吃螃蟹的人,目前,还是使用 dep
作为管理工具比较稳妥。
我们的系统使用截图方式获取验证码一直不靠谱,偶尔会报错。为了彻底解决这个问题,想着可以使用发送请求方式直接获取验证码。但是,又出现了新问题,对方服务器一直返回“验证码失效的错误”,弄了很久,最后发现,问题出在cookie的处理上。
获取验证码请求会设置 cookie。正常登陆过程,比如登陆页面的 webBrowser 初始 cookie=ck_1,当发送验证码后,webBrowser页面cookie=ck_2。因为我们登录使用webBrowser模拟的方式,而获取验证码使用的 http 直接请求的方式,所以,发送验证码请求获取验证码图片,我们还需要将当前 http 返回的 cookie 写到 webBrowser 的对应cookie 里。可以使用JS的方式:
document.cookie= c.Name=escape(c.value)
但是,这样就可以了吗?webBrowser 是根据 cookie 的路径取相应的 cookie 再发送到服务器进行认证的,但是,上面并没有设置 cookie 路径,而默认路径是在当前目录下,并不一定是根目录。所以,还需要设置 cookie 路径:
document.cookie= c.Name=escape(c.value);path=c.path
只有 cookie 的 name, path, domain 这三个属性都相同,才会被覆盖。取 cookie 的时候会先尾匹配 domian,然后前匹配 path。所以,domin 或 path 详细,当名称相同的时候会被优先使用。例如:
cookie_1:"document.cookie= name=sb1;path=/domain=csdn.net"
cookie_2:"document.cookie= name=sb1;path=/tt;domain=csdn.net"
同时存在的时候,会使用 cookie_2 的值。
可以通过设置 cookie 有效期,Expire time/Max-age 属性,来删除对应的 cookie。
使用 fiddler 分析 http 请求的时候,有的 cookie 后面有 HTTP-Only 标志,这是为了防止窃取 cookie 的安全机制,设置 HTTP-Only 后,使用 JS 不能访问该 cookie 。
一旦 cookies 通过 Javascript 设置后遍不能提取它的选项,所以你将不会知道 domain, path, expiration 日期或 secure 标记。因此分析 request 的 cookie 的时候,我们只能看到 cookie 的 name 和 value。
http://www.dannysite.com/blog/77/
http://blog.csdn.net/fangaoxin/article/details/6952954
lua:
luaJIT:
所以需要尽量通过 LuaJIT 的 FFI 来调用 C
lua code -> luaJIT 字节码 -> (热代码)对应的机器码
突然想起很久之前一次面试,面试官问我,当请求头没有 content-length
时,怎么知道请求体结束了?
http 的 header
和 body
之间空行分割的,又因为每个头部项是以 \r\n
作为结束符,所以,数据流中是以 \r\n\r\n
来分割解析请求头(响应头)与请求体(响应体)的。如下图所示:
那么怎么知道(请求体)响应体结束了呢? http 协议规定,响应头的字段 content-length
用来表示响应体长度大小,但是,有可能发送请求头时,并不能知道完整的响应体长度(比如当响应数据太大,服务端流式处理的情况),这时需要设置请求头Transfer-Encoding: chunked
,使用数据块的方式传输,数据块格式如下图所示:
每个数据块分为两个部分:数据长度和数据内容,以 \r\n
分割,最后长度为 0 的数据块,内容为空行(\r\n
),表示没有数据再传输了,响应结束。需要注意的是,此时, content-length
不应该被设置,就算设置了,也会被忽略掉。
回到最开始的那个问题,我当时对 http 协议不太清楚,回答不上来,那位面试官就告诉我,可以使用 \r\n\r\n
来判断,现在看来,他说的并不严谨。首先,http 协议并没有规定请求体(响应体)要以 \r\n\r\n
作为结束符,其次,很重要的一点是,响应体(请求体)的内容是多种多样的,你没法做限制,当数据内容包含\r\n\r\n
时,显然解析出来的响应体就是不全的。
当然,如果是自己实现 http 服务端的话,怎么兼容这种情况呢?
如果是短连接的话,比较简单,连接关闭就表示数据传输完成了。如果是长连接的话,一种不太优雅的方式就是使用超时机制,当读取超过一定时间,就认为数据已经传输完成。
总之,判断数据(块)结束最严谨的方式是计算长度,而不是使用结束符,但是,一般可控的场景下(双方约定),还是可以选择使用结束符来判断的,这样实现起来会更简洁。此时,为了防止内容中包含约定的结束符,导致数据内容被提前截断,客户端可以在发送数据时先对内容中的约定结束符进行编码。
因为服务端在解析请求头和请求体时,都需要依据以上协议,来读取完整数据。Slow Headers
与 Slow POST
两类 DDoS 慢速攻击正是利用了这个原理。
正常的 HTTP 报文中请求头部的后面会有结束符 0x0d0a(\r\n 的十六进制表示方式),
而攻击报文中不包含结束符,并且攻击者会持续发送不包含结束符的 HTTP 头部报文,
维持连接状态,消耗目标服务器的资源。
火焰图可以用来分析程序代码函数层的 CPU 或内存的使用情况,为程序的优化提供很直观的参考。
因此,对一个成熟的项目来说,能够随时收集运行时的火焰图必不可少。
得益于春哥对动态追踪技术的研究(动态追踪技术漫谈,建议先看一下),OpenResty 提供了很多调试工具,其中就包括火焰图收集脚本。
下载 OpenResty 项目源码,编译(configure
)带上 --with-debug
,开启调试模式编译;
安装 systemtap,以及内核调试信息,注意要跟内核版本保持一致:
uname -r #查看内核版本
apt-get install systemtap linux-image-`uname -r`-dbg linux-headers-`uname -r` #自动安装
下载调试工具 stapxx
git clone https://github.com/openresty/stapxx.git
cd ./stapxx
export PATH=$PWD:$PATH
收集 CPU
./samples/lj-lua-stacks.sxx --skip-badvars -x PID > a.bt #PID 即 nginx worker 进程 id
./fix-lua-bt a.bt > a.bt #美化数据,去除杂音 https://github.com/openresty/openresty-systemtap-toolkit#fix-lua-bt
收集内存
./samples/sample-bt-leaks.sxx -x PID -v > a.bt #PID 即 nginx worker 进程 id
下载 FlameGraph 将堆栈信息生成火焰图
tar -zxf FlameGraph.tar.gz
cd ./FlameGraph
./stackcollapse-stap.pl ../stapxx/a.bt > a.cbt
./flamegraph.pl --encoding="ISO-8859-1" --title="Lua-land on-CPU flamegraph" a.cbt > a.svg
使用浏览器打开 a.svg
即可得到火焰图
WARNING: Found 0 JITted samples.
这两个工具都被设计成分析非常繁忙的 nginx worker 进程(CPU 使用率接近
100%,每秒处理几百、几千、乃至几万个请求)时仍然开销极小(吞吐量极限的损失低于 5%),所以你只应当你的目标 worker 进程的
CPU 使用率足够高时(至少超过 10% 吧),才能得到比较有意义的结果。它们是通过 OS 内核 tick 或者 cpu clock 探针按
CPU 时间进行分时采样的,而并不是追踪每一个具体的请求(此种做法开销很大)。
程序中引用了 lua cjson
包,使用 stapxx
工具抓取时可能会出现链接或函数找不到之类的错误。此时,需要重新编译 cjson
:
git clone https://github.com/openresty/lua-cjson.git
cd lua-cjson/
LUA_VERSION = 5.1
TARGET = cjson.so
PREFIX = /usr/local/openresty/luajit
CFLAGS = -g -Wall -pedantic -fno-inline
CFLAGS = -O3 -Wall -pedantic -DNDEBUG
CJSON_CFLAGS = -fpic
CJSON_LDFLAGS = -shared
LUA_INCLUDE_DIR ?= $(PREFIX)/include/luajit-2.1
LUA_CMODULE_DIR ?= /usr/local/openresty/lualib
LUA_MODULE_DIR ?= $(PREFIX)/share/lua/$(LUA_VERSION)
LUA_BIN_DIR ?= $(PREFIX)/bin
注意看错误信息,出现连接符或未知函数变量之类的错误一般都是对应的组件没有调试信息,重新编译对应的组件,编译时开启调试模式即可。
第一次参加技术性的大会,总体质量挺高的,有些技术实践,技巧建议或者技术前瞻很有意义,收益颇多,也很受启发,这种技术性大会,可以的话,打算每年参加一场(立个 flag
:)),原因有二:
记下一些技术点,后续有时间需要慢慢研究。
Hyperscan
高性能的正则表达式匹配库PCRE
库快 30% 左右。Hyperscan 已经开源,是一款来自于Intel的高性能的正则表达式匹配库。它是基于X86平台以PCRE为原型而开发的,并以BSD许可开源在 https://01.org/hyperscan。在支持PCRE的大部分语法的前提下,Hyperscan增加了特定的语法和工作模式来保证其在真实网络场景下的实用性。
resolver
命令扩展,支持 local
指定 DNS 文件
# read nameserver address from /etc/resolv.conf
resolver local=on ipv6=off;
resolver local=/path/to/resolv.conf
-------------------------- 第二天------------------------
cloudflare
使用ngx.time
会被缓存,只有开始处理事件时才会被更新,使用 ngx.update_time()
lj_str_new
string.byte
代替 string.sub
ngx.null
而不是 nil
local dump = require "jit.dump"
dump.on(nil, "/tmp/jit_dump.log")
我并不是 Go 方面的专家,只是大略列表一下 OpenResty 相对于 Go 的优势:
1. Lua 是全动态语言,支持动态加载和卸载代码,可以在请求级别完成。Go 世界的一些哥们尝试过把动态语言 VM 嵌入进 Go 或用 Go
来实现上层语言的解释器,性能都极差。因为 Go 是很封闭的运行时环境,并不能方便而高效地进行嵌入或扩展。
2. 享受 NGINX 完整的基础设施和生态圈。像二进制热升级和 0 下线时间的整体配置 reload 这样的功能都是由 NGINX 直接提供。
3. 单线程编程方式,方便用 C 扩展,同时通常不用考虑线程安全的问题,开发成本大大下降。而 Go 是多线程模型,对于 C
扩展而言,要么得确保线程安全,要么就要加一把大锁。当然 cgo 还有更多的限制了。
4. 根据我们之前线上观察的规则,Go 运行时的线程调度器的 CPU 损耗非常大,而且很难优化。
5. Go 在底层实现上继承了 Plan9 系统的很多古老的设计,我之前看连 ABI 都是 plan9
的。这使得很多现有的调试和动态追踪工具链和 Go 系统存在较大的兼容性问题。我记得我在上一家东家分析那些运行缓慢的 Go
进程时,都只能在系统调用及其以下层面进行分析,用户态是一团乱麻,非常郁闷。
总而言之,Go 在我看来是一个很尴尬的抽象层面,一方面它不及 C/C++ 那么底层那么有控制力,另一方面它又不及 Lua
这样的动态语言那么灵活,那么动态。更要命的是它的设计是相当封闭的(不封闭也没有做它的那些抽象了),不像 nginx 或 luajit
那样方便深度定制和扩展。
当然了,Go 用作简单的服务还是比较方便的,但如果一旦业务复杂度上去了,就会越写越纠结,至少这是我看到的前东家的一些 Go 粉工程师用 Go
做公司项目之后的痛的领悟。相比之下,Rust 看起来还更有前途一些,可以用作 C++ 的替代物,当然和 OpenResty
也不在一个抽象层面上就是了。而 Erlang 的性能我也不敢恭维了,嘿嘿。
当然了,作为 OpenResty 的作者,我的观点肯定是具有偏向性的,仅供参考。
Regards,
Yichun
引用
《微服务架构与实践》算是入门级的书,除了代码,就没多少页了,但对于新手还是很合适的,里面的一些概念,框架,以及架构等都很有启发作用。所谓的微服务架构,其实一直就是在应用到,这本系统的梳理了这些技术,使得理解更深入了。
我认为微服务最终要的是模块化和自动化,模块化就是系统分解,自动化包括自动生成代码,自动测试,自动部署,自动报警等。
微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。
尽管“微服务”这种架构风格没有精确的定义,但其具有一些共同的特性,如围绕业务能力组织服务、自动化部署、智能端点、对语言及数据的“去集中化”控制等等。
微服务架构的思考是从与整体应用对比而产生的。
微服务是互联网发展的必然结果,软件规模越来越大了,维护起来也越来越费力,互联网讲究的是速度,快速开发快速上线验证。而传统的开发模式,更多是阶段流程式:需求分析,概要设计,详细设计,编码,测试,交付,验收,维护等。显然太慢了,也不够灵活,互联网更多的情况是尝试,需要先出一个简单版本,在快速迭代推进。
随着软件行业的发展,有很多成熟,开源的组件备应用,模块化的系统可以很好的将这些好的组件应用在自己的系统当中,更新,维护也很方便。
优点:
缺点:
自己之前对测试不太重视,总以为代码写好了就行,也不知到怎么写测试。换新工作之后体会到了测试的重要性,尤其是自动化测试的概念,感觉以前自己就像猴子。
测试是微服务架构很重要的一个部分,因为涉及到很多模块,复杂的异步逻辑等,调试和 bug 修复难度都更大,所以越早测试越容易解决问题,单元和接口测试是必须的。
<iframe src="https://drive.google.com/file/d/0B_bGvu4-BQOCTERxMXc2ZW1kSW8/preview" width="400" height="300"></iframe>自动化测试是持续集成开的保障,不然增加新功能又会导致前面的功能出现问题,测试成本是巨大的,我是经历过的,之前的项目就是没有单元,只有最终的功能性测试,总是重复测试,重复出现问题,举步维艰,不但效率低下,还很打击士气。
我们现在的项目使用 python 写单元测试,GitLab CI 自动化测试,方便多了。
虽然之前也一直认为在程序开发过程中,编码应该只能占很小的部分,大部分时间应该花在设计,测试,以及上线之后的问题发现与解决当中,但是实际却很难做到,缺少方向和方法。
人跟动物最大区别就是人可以创造和使用工具,我想新手和老司机的区别也在此吧。学会使用工具,可以极大的提升工作效率,解放自己,最近在使用swagger 设计 API,可以方便的设计出符合 RESTful 规范的API。后面需要多关注,尝试新的框架和技术。
encoding/json 是 Go 代码经常使用的包,但是,可能很多人都会忽略下面这段说明:
To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:
bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null
当 json 解码到 interface 类型的变量值时,会将 JSON numbers(实质是 string 类型,表示整数或浮点数数字字符串)都当作类型 float64 存储。
试想以下代码输出?
package main
import (
"fmt"
"encoding/json"
"reflect"
)
func main() {
s := `{"name":"test","it":1021,"timestamp":1557822591000,"mmp":{"a":"ax","b":999999}}`
type st struct{
Name string
Timestamp interface{}
Mmp interface{}
It interface{}
}
var tmp st
err := json.Unmarshal([]byte(s),&tmp)
fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)
fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)
}
tmp: {Name:test Timestamp:1.557822591e+12 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
float64, 1.557822591e+12
完成 Json 解码后,Timestamp
类型为 float64。这显然是无法让人接受的,就这里来说,时间戳应该是 int64 才对。目前,有两个解决办法:
避免使用 interface,而是直接静态类型指定,在大多数情况下,Json 字符串结构都是已知的,静态的。
上面的场景,就可以将时间戳属性定义为 Timestamp int64
。
func (*Decoder) UseNumber() 使解码器将数字作为 json.Number 类型,
而不 float64 解码到 interface 变量。
...
ds := json.NewDecoder(strings.NewReader(s))
ds.UseNumber()
err := ds.Decode(&tmp)
fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)
//rf, _ := strconv.ParseFloat("123.90",64)
fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)
tmp: {Name:test Timestamp:1557822591000 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
json.Number, 1557822591000
可以看到,json.Number 其实就是字符串类型:
type Number string
因此,这里其实就是保留原始字符串,延迟解析。在需要的时候,使用提供的函数 Float64()
, Int64()
等转化成对应的类型,其实,这些函数的实现就是使用 strconv
包将字符串转化成整型或浮点型。
但是,这里引入了一个新的类型 json.Number,会侵入到别的无关的代码中,也就是说,可能会导致,在其它模块,不得不在类型判断时,加入 json.Number case。这种耦合是比较让人难受的。
遗憾的是,目前看来,只有这两种方式了,虽然都不够优雅。
这是个很奇怪的问题,因为技术上来说,将数字字符串分别解析为整型或浮点型并不难实现,Go 编译器就很好的实现了(想想 x:=100
与 x:=100.0
的区别);
而且,如果 Json 数字的含义是整型,默认却解析成 float64 就会有精度丢失的问题,因为 int64 比 float64 表示的范围更大。
去 Go issues 找了下,也并没有看到合理的解释,难道只是为了实现方便,偷了个懒?真是个奇怪的坑!
相关 issues:
float64 的表示范围显然远大于int64/uint64 (ref. math. MaxFloat64, math. MaxInt64),只是在表示整数时有可能有精度损失。
JSON (json.org)并没有规定number的精度和大小范围,所以即使用uint64或int64,在解析整数时仍然存在溢出的可能。这时如果用float64来解析,因为表示范围大于int64,溢出的可能性更小,所以更安全(精度损失总比溢出强)。如果追求完全不溢出,可以用 type Number string。
Go 中的 float64 其实等同于 double
类型:
1bit(符号位) 11bits(指数位) 52bits(尾数位)
范围是 -2^1024 ~ +2^1024
,也即 -1.79E+308 ~ +1.79E+308
。精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响:2^52 = 4503599627370496,一共16位,因此,double的精度为15~16位。而 int64 等同于 long, 占8个字节,表示范围: -9223372036854775808 ~ 9223372036854775807
。
因此,出现这个坑的原因,是设计上的取舍,为了保证 Json 数字解析安全(不溢出),只能牺牲精度。
nginx-1.9.11 开始,支持动态加载源码模块或第三方模块,需要先在编译 Nginx (./configure)时指定:
./configure --with-mail=dynamic ...
./configure --add-dynamic-module=...
模块对应的 .so
文件会被存放在 /path/nginx/modules/
下面,当需要使用模块时,在 nginx.conf
最顶端(main)配置 load_module,指定模块路径。
但是,这时,添加模块仍然需要在编译阶段声明,即需要重新编译 Nginx 程序,很多时候,在生产环境是不能随便去更新替换二进制程序的。
因此,在 nginx-1.11.5 编译命令增加了 --with-compat
,可以单独编译需要新增的模块,直接动态加载到原有的 nginx 二进制程序,而不用重新编译 nginx!
官方阐述:
Dynamic modules – NGINX 1.11.5 was a milestone moment in the development of NGINX. It introduced
the new --with-compat option which allows any module to be compiled and dynamically loaded into a
running NGINX instance of the same version (and an NGINX Plus release based on that version).
There are over 120 modules for NGINX contributed by the open source community, and now you can load
them into our NGINX builds, or those of an OS vendor, without having to compile NGINX from source.
For more information on compiling dynamic modules, see Compiling Dynamic Modules for NGINX Plus.
下面,以动态加载模块 lua-nginx-module
为例,展示具体用法。
wget http://nginx.org/download/nginx-1.11.5.tar.gz
tar -xzvf nginx-1.11.5.tar.gz
include
和 lib
路径,编译的时候要用到。 export LUAJIT_INC=/usr/local/include/luajit-2.0
export LUAJIT_LIB=/usr/local/lib
--with-compat
开启兼容模式./configure --prefix=../nginx-dy/nginx --with-ld-opt="-lpcre -Wl,-rpath,/usr/local/lib" \
--with-pcre=../pcre-8.42 \
--with-zlib=../zlib-1.2.11 \
--with-stream \
--with-http_ssl_module \
--with-mail=dynamic \
--with-compat
make -j4
make install
nginx/sbin/ngin -t -- 测试安装成功
动态加载
.so
文件cd ./nginx-1.11.5/
./configure --with-compat --with-cc-opt='-O0 -I /usr/local/include/luajit-2.0' \
--with-ld-opt='-Wl,-rpath,/usr/local/lib -lluajit-5.1' \
--add-dynamic-module=../lua-nginx-module-0.10.13 --add-dynamic-module=../ngx_devel_kit-0.3.0
make modules
cp objs/ndk_http_module.so objs/ngx_http_lua_module.so ../nginx-dy/nginx/modules/
nginx.conf
最顶端,添加:load_module modules/ndk_http_module.so;
load_module modules/ngx_http_lua_module.so;
with-compat
,否则运行 Nginx 时,会报 ngx_lua 模块不兼容的错误。--with-ld-opt
指定 PCRE 等依赖组件 lib 安装路径,否则会报动态库链接符号错误。Nginx 动态加载模块,使增加功能模块变的更加方便与灵活,由于,Nginx 奇数版本为开发版,偶数为稳定版,目前,虽然稳定版已经到了 nginx-1.14.0
,但是,大部分生产环境应该还是 nginx-1.10.xx
,所以,要使用这个功能呢个,生产环境需要升级到 nginx-1.11.5 或以上版本才行。 :)
创建虚拟机设置磁盘大小时,很难预测未来的使用,导致分配的磁盘大小后续常常不够用。
可以使用命令 df -h
查看挂载的目录空间使用情况。
因为我的根目录(boot 目录,挂载到 /dev/sda1
分区)空间不够,导致软件安装失败,出现空间不足的错误。扩容之后效果下:
1)对虚拟机备份: 直接将虚拟机所在的文件夹复制一份就行;
2)关闭虚拟机,扩展虚拟机磁盘容量:虚拟机 -> 设置 -> 扩展 -> 设置最大磁盘大小
注意,此时虽然虚拟机磁盘更大了,但是并没有挂载进系统,所以,系统还不能识别应该到。可以使用命令 fdisk -l
看到,总的磁盘大小变大了,但是分区大小没有变,增加空间“不见了”。
3)安装磁盘分区工具:GParted
,调整分区大小。
有两种安装方式
使用命令 apt-get install gparted
直接安装;
调整非系统分区,可以使用这种方式,更简单,快捷。
下载 iso 镜像
调整系统分区大小,必须使用这种方式安装。
4)加载 gparted 的 .iso 文件,选择光驱启动虚拟机。
5)调整分区大小
这里需要特别解释一下,磁盘和分区的关系,就像堵车时,路跟车子关系类似,前面的车子必须预留空间给后面车子,后面的车子才能往前移动。
Free space preceding
就是空出给后面分区的空间大小
因此,如上图,我要调整“最后面的” /dev/sda1
大小,就必须先按顺序调整“前面的” sda8,sda7 等等分区的大小。
注意, New Size
是当前分区的大小,这个值可以不变或调大,但不要调小,否则可能导致数据丢失。
6)设置完成后,记得点击 Apply
应用,分区需要花点时间,耐心等待~
7)重启虚拟机, /dev/sda1
分区调整成功
end.
ab -n100000 -c600 -H 'Host: httpbin.org' http://192.168.3.22:9001/test_limit > razor-1.0_abtest.out
-n 总请求数
-c 并发请求数
-H 请求头,可使用多次,可重复覆盖
并且将测试报告重定向到文件 razor-1.0_abtest.out
-c
指定。Time taken for tests / (Complete requests /Concurrency Level)
Time taken for tests / Complete requests
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 192.168.3.22 (be patient)
Server Software: openresty/1.11.2.2
Server Hostname: 192.168.3.22
Server Port: 9001
Document Path: /test_limit
Document Length: 233 bytes
Concurrency Level: 600
Time taken for tests: 218.767 seconds
Complete requests: 100000
Failed requests: 99937
(Connect: 0, Receive: 0, Length: 99937, Exceptions: 0)
Non-2xx responses: 100000
Total transferred: 16519656 bytes
HTML transferred: 1813545 bytes
Requests per second: 457.11 [#/sec] (mean)
Time per request: 1312.604 [ms] (mean)
Time per request: 2.188 [ms] (mean, across all concurrent requests)
Transfer rate: 73.74 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 2 72.2 1 3010
Processing: 11 1295 1913.9 289 9323
Waiting: 6 961 1646.4 202 6593
Total: 11 1297 1916.2 289 9485
Percentage of the requests served within a certain time (ms)
50% 289
66% 328
75% 3197
80% 3242
90% 3336
95% 6273
98% 6314
99% 6336
100% 9485 (longest request)
最近遇到在Linux下使用C++标准正则库报错,google发现是与gcc版本的问题,于是只好重新安装编译新的gcc,但是安装过程没有想象的那么简单,以下是遇到的各种坑:
注意:网上很多垃圾教程,很是误导人,就严格按照下面一步步来,不要试来试去,陷入各种奇怪错误陷阱,坑自己。
1)下载gcc源码,不多讲。
2)root权限切换到/opt目录下
cd /opt
解压gcc源码包到/opt目录下
tar xzvf gcc-4.8.2.tar.gz
cd gcc-4.8.2
3)下载依赖包 gmp, mpfr, mpc
分别下载安装,很麻烦,很作死,不多讲。
执行下面的脚本自动下载关联依赖库
./contrib/download_prerequisites
4)新建一个文件,用来编译
一定要不要在那个gcc文件下直接编译,否则会报错,作死妥妥的
cd ..
mkdir objdir
mkdir /usr/local/gcc-4.9.1
mkdir /usr/local/gcc
5)编译gcc
cd objdir
../gcc-4.9.1/configure --prefix=/usr/local/gcc-4.9.1 --exec-prefix=/usr/local/gcc --enable-languages=c,c++
make #很慢,大概要编译一个多小时
6)安装gcc
make install
以上gcc就算是安装完了,但是要正常使用,还需要一些配置,一下配置方法引用自 http://ilovers.sinaapp.com/article/centos%E4%B8%8B%E5%AE%89%E8%A3%85gcc-481:
make install 之后,会发现 /user/local/gcc 下放置的是 bin + lib 文件,/usr/local/gcc-4.8.1 下放置的是 include 文件。上面完事之后,就是删除原有的 gcc,替换成现在的最新版本;不过为了保险起见,还是将原有的 gcc 换成其他名字的好,比如 gcc-4.4.7/g++-4.4.7。关于后续的工作其实还有一些,主要是环境变量的设置,以及为 c++ 做的一些设置。
# 将 gcc/g++ 改名
$ mv /usr/bin/gcc /usr/bin/gcc-4.4.7
$ mv /usr/bin/g++ /usr/bin/g++-4.4.7
# 环境变量的设置
$ export PATH=/usr/local/gcc/bin:$PATH # 可以让 us 使用最新的 gcc/g++;
$ export LD_LIBRARY_PATH=/usr/local/lib # 这个可能不是必须的,对于 me 来说是必须的,设置的是 lib 的搜索 path;
$ ln -s /usr/local/gcc-4.8.1/include/c++/4.8.1 /usr/include/c++/4.8.1 # 在 include/c++ 文件夹下添加最新的 c++ 4.8.1 版本(这是个符号链接);
$ export C_INCLUDE_PATH=/usr/include # 这个是多余的,实际上不用设置;
$ export CPLUS_INCLUDE_PATH=/usr/include/c++/4.8.1:/usr/include/c++/4.8.1/x86_64-unknown-linux-gnu # c++ include 搜索目录,这里有两个,使用的 : 隔开;
有个问题是,在 shell 中通过 export 设置的环境变量不是持久有效的,在用户退出登录之后就不再有效,可以将 export 的环境变量在用户主目录下的 .bash_profile 中设置,对用户来说,是持久有效的;如果想对对所有的用户有效,需要 root 在 /etc/profile 中设置;
安装可能出现的问题以及方案
configure 步骤提示找不到 gmp、mpfr 等 lib 或是 header;缺少的要安装,可以使用自带的包管理器,比如 yum install gmp,也可以从官网下载安装,下载地址:
GMP:http://gmplib.org
MPFR:http://www.mpfr.org
MPC:http://www.multiprecison.org
ISL+CLooG:ftp://gcc.gnu.org/pub/gcc/infrastructure
ISL 明明已经安装了,然而 configure 检测 no !设置环境变量 $ export LD_LIBRARY_PATH=/usr/local/lib (这是 isl lib 所在的目录,当然 u 的可能不一样);
stubs-32.h 找不到,安装 32 位的 glibc-devel;
编译 c++ 发现找不到 c++config.h;本来 c++ include 目录是 /usr/include/c++/4.8.1,c++config.h 位于其下的 x86_64-unknown-linux-gnu (这个文件夹跟平台有关)下,所以可以在 CPLUS_INCLUDE_PATH 中设置;
环境变量设置只在 shell 中有效,退出之后就不再有效;修改 ~/.bash_profile 文件,在其中添加环境变量(需要退出登陆有效);
测试
编译一下 hello.cpp,使用了 c++11 的一些特性,比如初始化方式,类型推断以及新的 for 用法,$ g++ -std=c++11 hello.cpp :
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> v = {2, 4, 8, 3, 5, 6, 1, 7, 10, 9};
sort(v.begin(), v.end());
for(auto i: v)
cout << i << " ";
cout << endl;
return 0;
}
以上。
lua-resty-waf 是基于 OpenResty
开发的 WAF 项目,其核心的防护规则策略基本与 ModSecurity Core Rule 一致,但是具体实现有所不同。
下面主要分四块阐述其实现功能与原理:
系统配置分为两块系统基本配置和规则配置。
规则配置
系统默认提供了基础的防护规则集,规则集文件都在 rules/
文件夹下,默认有九个文件:
11000_whitelist.json
20000_http_violation.json
违反 HTTP 协议防御21000_http_anomaly.json
异常 HTPP 请求防御35000_user_agent.json
user_agent 防御40000_generic_attack.json
一般攻击防御41000_sqli.json
SQL 注入防御42000_xss.json
XSS 攻击防御90000_custom.json
客户自定义防护规则99000_scoring.json
SCORE 阀值控制默认规则集的执行顺序也是从上到下的,注意文件的命名规律,后续添加自己的规则集的时候最好遵循这种规范。
规则配置的加载方式有三种:
1)系统启动时默认加载
waf.init
函数默认会去 package.path
前缀路径下的目录 rules/
下加载数组 global_rulesets
指定的 .json
规则集配置文件。 global_rulesets
默认就是包括了 rules/
目录下的所有文件名,也是基本的系统参数之一,可以通过 waf:set_option("global_rulesets", {})
来指定,具体后面会说。
2)使用 load_secrules
加载
可以调用函数 load_secrules 函数从磁盘加载 ModSecurity SecRules
配置文件,参数就是文件所在的绝对路径。需要注意的是还需要调用函数 add_ruleset
将规则集名(文件名称)注册到系统,否则系统是识别不到的。
系统内部会按行将 ModSecurity
规则集文件转换(在 translate
包内)成 waf 对应的规则格式(json)。目前支持四种规则指令:
3)使用 add_ruleset_string
加载
add_ruleset_string 可以直接加载规则集字符串(json)。
用户可以使用这种方式动态的加载自定义规则集合。
在每个阶段执行对应的规则集之前,都会先合并(merge)规则集,主要就是根据规则集名,当规则集名称一样时,用户自己添加的规则集优先级更高。
系统配置
waf.new
函数会初始化一个系统基础参数表,这些参数都可以通过函数 waf.set_option(参数名称, value)
来设置。
一些比较重要的参数说明:
_debug
开启 debug 模式,将会打印更详细的日志,默认 false
_debug_log_level
debug 模式的日志输出级别,默认 ngx.INFO
_deny_status
请求被规则拒绝时返回的状态,默认 403
_event_log_altered_only
是否只有当请求结束时(DENY
或 DROP
)才对外输出日志数据,默认 true
_event_log_level
设置日志输出级别,默认 ngx.info
_event_log_request_*
可以指定对外日志输出 arguments
、body
、headers
字段,默认都是 false
_event_log_target
设置日志对外输出的方式,有三种方式可选(详见日志模块说明),默认 error
_mode
系统运行模式,有三种可选值,默认值 INACTIVE
SIMULATE
默认值,模拟模式,只会记录规则命中日志,不会执行规则 action
INACTIVE
不执行规则引擎,即 不执行 exec
函数ACTIVE
即正常模式_score_threshold
风险最大阀值,当大于该值时,请求将会被 DENY
规则模块是 WAF 项目的核心,包括解析和执行两个部分,为了支持类似 ModSecurity 的防护规则,规则配置比较复杂,解析和执行逻辑就更复杂了。
规则解析
规则配置是按规则集(规则数组)的形式被读取的,规则集再分为多个阶段 --- access
,header_filter
等。所有的规则集在解析时,会按阶段的维度,添加到对应阶段的规则集数组。
需要注意的是,规则解析时会计算两个特殊的变量值:
rule.offset_nomatch
数值,当当前规则匹配失败时,规则遍历迭代器接下来要跳转的规则数,即:当前规则序数 + offset_nomatch
= 下条规则的序数rule.offset_match
数值,当当前规则匹配成功时,规则遍历迭代器接下来要跳转的规则数这两个变量值一般都是 1
,即直接进入相邻的下个规则,但是使用 skip
或 CHAIN
都会改变这些值。他们都被放入 table 对象 rule
,供规则执行时使用。
action
规则行为定义
nondisrupt
map 数组,非破坏请求行为,定义命中当前规则后的数据行为,可与 disrupt
配合使用,在 disrupt
之前被执行
action
指定具体的行为,有九个可选值:
setvar
设置 K-V 值,默认将会存放到变量 storage
(table 类型),可作为中间缓存,生存周期是当前请求(ngx.ctx)initcol
持久化存储,将指定的值做存放到 redis
、memcached
或 dict
sleep
调用 ngx.sleep
status
设置当前请求被 DENY
后,响应的 HTTP 状态rule_remove_id
临时(内存)移出 data
(规则 ID) 对应的规则, 与 ignore_rule
原理相同mode_update
更新 _mode
(系统运行模式) 值data
上面 action 行为的参数值,动态数据类型,根据 action,可以是 map,字符串或者数值
col
设置存放到 storage
的 一维 key
inc
累加,当 value
为数字时,会将数值 value
累加到对应的中间缓存值key
设置存储的二维 key
value
设置存储的值,数值或字符串disrupt
字符串,定义具体防护方式,有六个可选值:
ACCEPT
结束当前阶段,继续执行下一阶段,目前因为规则都集中在 Acess 阶段,可以认为直接通过(PASS) WafDENY
拒绝当前请求,默认返回 403,返回状态可以在 nondisrupt.action.status
指定,但是不建议修改DROP
断开当前请求连接,特殊的 444 状态,Nginx 将直接断开连接,而不响应任何字节给客户端IGNORE
忽略该规则的本次命中,继续后面规则的校验SCORE
调整(加减)风险数值(anomaly_score
),只有当风险数值大于阀值(由配置 _score_threshold
指定)请求才会被拒绝,与 nondisrupt
配合使用CHAIN
规则链,与其他防护方式的规则组合使用,相当于后续规则的前置条件,类似 and 操作id
数值,唯一的标识当前规则op_negated
否定规则匹配结果,即对匹配结果取反operator
操作符,可选值:
REGEX
正则匹配,如果待匹配项为 table
,则会逐次匹配,一旦匹配成功就返回,下同REFIND
查找(ngx.re.find
)EQUALS
相等GREATER
大于LESS
小于EXISTS
在字符串数组 pattern
内存在指定的字符串CONTAINS
在获取的字符串或字符串数组内包含指定的 pattern
STR_EXISTS
在指定的字符串内存在
STR_MATCH
字符串匹配PM
字符串匹配,可同时与组内所有子串进行匹配(Aho–Corasick 算法)CIDR_MATCH
IP 地址匹配,当前 IP 是否在pattern
IP 数组内DETECT_SQLI
SQL 攻击检查DETECT_XSS
XSS 攻击检查VERIFY_CC
验证信用卡号是否合法pattern
匹配值,字符串或字符串数组,可以是具体的值或者正则表达式,与待匹配值(根据下面的 vars
计算所得)进行比较skip
数值,指跳过的规则个数(下个规则位置 = 当前规则位置 + skip + 1
)skip_after
数值,根据规则 id,直接跳转到对应规则vars
对象数组,定义待匹配值的获取方式
type
定义数据源,可选值(部分):
REQUEST_HEADERS
获取请求头,map 类型METHOD
获取请求方法,字符串类型,例如:GET,POST 等 HTTP 标准方法TX
获取中间缓存值(ctx.storage["TX"]
),map 类型URI_ARGS
获取请求参数(table),map 类型QUERY_STRING
获取请求参数,字符串,示例:a=1&b=2
REQUEST_BODY
获取请求 body 部分,map 或字符串类型URI
获取请求原始路径部分(ngx.var.uri),字符串REQUEST_URI
获取请求 URL,包括参数部分,字符串,示例:/a/b/c?a=1&b=2
COOKIES
请求 cookies
(table),map 类型REQUEST_ARGS
对象(map)类型。包括 URI_ARGS
, REQUEST_BODY
, COOKIES
三项值,但最终转化成一维 mapREMOTE_ADDR
获取请求端 IP 地址(remote_addr),字符串HTTP_VERSION
获取 HTTP 协议版本,数值,可选值:2.0, 1.0, 1.1SCORE_THRESHOLD
获取当前风险阀值,数值类型ARGS_COMBINED_SIZE
获取请求参数和请求 body 的字节大小,数值TIME
字符串,格式:“时:分:秒”TIME_EPOCH
获取当前时间戳,精确到秒,数值类型storage
是否跳过缓存,根据 vars
的定义,重新计算待匹配值,存在(not nil)即为 true
。因为请求处理过程数据源可能会修改数据源的某些值,导致缓存不一致的情况。需要注意的是这里的缓存,仅仅是存在当前请求内。parse
字符串数组,长度固定为 2
,定义从数据源取出待匹配数据集的规则
specific
取出参数指定的值regex
正则匹配,取出所有正则匹配参数的数据集keys
取出数据源中所有的 key
,作为数据集values
取出数据源中所有的 value
,作为数据集all
将整个数据源作为数据集1
,表示无意义unconditional
指示当前规则将一定会被命中opts
transform
字符串或字符串数组,对上述数据源进行转换的方式,有如下可选值(部分):
uri_decode
uri 解码lowercase
转成小写md5
计算 md5nolog
不记录该条规则的命中日志logdata
设置规则命中日志字段 logdata
的值,可以使用具体的值或者变量(%{value}
),实例:"logdata" : "%{TX.anomaly_score}"规则执行
规则是在 waf.exec
函数内执行的,每个阶段只会执行当前阶段对应的规则集,及规则集里面的规则。需要注意的是 CHAIN
规则的执行逻辑,这种类型的规则会组成一个规则链,规则链内的规则是 and
关系,即规则匹配失败,就会跳过当前整个规则链。规则链是怎么组成的呢?当遇到非 CHAIN
规则时,就会计算成一个规则链。
下面使用 C
代表 CHAIN
规则,X
代表非 CHAIN
规则,有如下规则集:
C C C X X C X
将生成两条规则链:
CCCX
CX
规则命中后,都会将命中(匹配成功)规则日志记录到日志输出缓存数组,除了 CHAIN
类型的规则,也就是说规则链命中后只会记录一条日志。
规则日志可以在阶段结束时输出,也可以在请求结束时,汇总一起输出。
日志是以 json
格式输出的,包括以下字段:
timestamp
当前时间戳(秒)client
请求客户端地址(remote_addr
)method
请求方法uri
请求路径alerts
数组,规则命中记录
id
命中的规则 idmsg
规则说明match
规则操作符(operator
)函数返回的第二个值,这个值非常灵活,可以是字符串,数字或者数组logdata
规则 logdata
配置指定的输出项,比如当前阀值等id
唯一的标记当前请求 ID,首次调用函数 waf.new
时随机生成id
唯一的标记当前请求 ID,首次调用函数 waf.new
时随机生成uri_args
map 类型,请求参数;可选,由参数 _event_log_request_arguments
控制request_headers
map 类型,请求头;可选,由参数 _event_log_request_headers
控制request_body
map 或字符串 类型,请求体;可选,由参数 _event_log_request_body
控制ngx
数组,可选,由参数 _event_log_ngx_vars
指定的变量(ngx.var
)值日志对外输出方式由 _event_log_target
设置,有三种输出方式:
error
直接使用 ngx.log
输出file
输出到参数 _event_log_target_path
指定的文件内socket
使用库 resty.log
输出到指定的日志服务器,需要配置相关参数,初始化 log.socket
客户端虽然,系统支持通过函数 load_secrules
直接加载 ModSecurity
规则集文件,但是,最好别这么干,因为自动转换过程交繁琐,不小心就容易出错。
Revision
表示改动序号(ID),每次 KV 的变化,leader 节点都会修改 Revision
值,因此,这个值在 cluster(集群)内是全局唯一的,而且是递增的。
需要特别说明的是,Revision
、ModRevison
与 Version
三者之间的区别:
ModRevison
记录了某个 key 最近修改时的 Revision
,即它是与 key 关联的。Version
表示 KV 的版本号,初始值为 1,每次修改 KV 对应的 version 都会加 1,也就是说它是作用在 KV 之内的。使用参数 --write-out
可以格式化(json/fields ...)输出详细的信息,包括 Revision
、ModRevison
与 Version
etcdctl get foo --write-out=fields
引用:
buffer工作原理
首先第一个概念是所有的这些proxy buffer参数是作用到每一个请求的。每一个请求会安按照参数的配置获得自己的buffer。proxy buffer不是global而是per request的。
proxy_buffering 是为了开启response buffering of the proxied server,开启后 proxy_buffers 和proxy_busy_buffers_size 参数才会起作用。
无论proxy_buffering是否开启,proxy_buffer_size(main buffer)都是工作的,proxy_buffer_size所设置的buffer_size的作用是用来存储upstream端response的header。
参考目前大多数的开源 WAF 项目,并没有对这块有特别的调整,即都是使用的默认值。
默认 proxy_buffering 是开启的,开启 buffer 能有效的提高请求交互效率,特别是对于客户源站响应比较慢的情况。
综上,这块参数调整如下:
proxy_buffer_size 8k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
proxy_buffering on;
worker_processes 启动 worker 进程数,为了减少上下文切换,最好等于系统内核数,提高处理效率。auto
将自动设置成内核数。
worker_cpu_affinity 将进程绑定到固定的内核。auto
自动绑定到可用的内核。
注意,如果是容器部署的话可能会有问题,因为默认情况下使用主机 CPU 资源是不受限制的,当然你可以启动的时候指定 CPU 使用数量限制。
worker_processes auto;
worker_cpu_affinity auto;
开启 pcre_jit
pcre_jit on;
但是,这只是对配置解析时已知的正则开启 “just-in-time compilation” (PCRE JIT)。
如果要在 ngx_lua 模块中启用 PCRE JIT,需要源码编译 pcre 时,指定 --enable-jit
,具体见春哥回答。
具体编译命令,严格按照这种方式编译,完成后,实用工具 ngx-pcrejit(./ngx-pcrejit -p 7566
),验证 ngx_lua 是否启用 PCRE JIT。
默认 Nginx 不会立即将足够小的数据包发送出去(Nagle 算法),而是最多等待 0.2s,待数据包达到足够大时才一起发送,类似缓存机制,可以提高网络效率。
tcp_nopush 优化一次发送的数据量,需要与 sendfile
配合使用。
tcp_nodelay 禁用延迟发送(Nagle 算法)。
两者看似矛盾,但是可以一起使用,最终的效果是先填满包,再尽快发送。
tcp_nopush on;
tcp_nodelay on;
sendfile on;
配置优化如下:
proxy_connect_timeout 65;
proxy_read_timeout 65;
将重复请求的资源缓存到我们(Razor)这边能有效的较少源站压力,且提高客户端响应速度。但是,会带来一定副作用,比如,资源无法及时更新;缓存的资源不够精确,与请求需要的资源不符合等等问题。所以,建议根据源站实际情况,作为 razor 配置项,由用户(管理员)来配置
proxy_cache_path 指定缓存保存路径
expires 设置浏览器缓存失效时间
配置参考:
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:100m inactive=1d max_size=1g;
proxy_cache_key $host$uri$is_args$args;
server {
location / {
...
proxy_cache one;
proxy_cache_valid 200 304 10m;
proxy_cache_valid 301 302 1h;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
#proxy_cache_valid any 1m;
expires 12h;
...
}
目前,如果客户端支持压缩编码(带有请求头:Accept-Encoding,浏览器默认都支持),而且用户源站也支持对应的压缩编码格式,则返回的响应就是已经被压缩编码的。
如果,Razor 开启 zip (gzip on;)有效的唯一场景是,客户源站不支持压缩编码格式,我们(Razor)来帮它压缩。因为,客户源站一般都是支持压缩编码的,而且,在我们这边压缩会消耗 CPU,且只能节省我们到客户端的带宽资源,并不划算。而且参考其他 WAF 实现,也都没有开启 gzip。
至于 https://www.oschina.net/question/17_3971 这里说的所有请求,我们这边代理到客户源站时,都添加头(Accept-Encoding),即默认所有的请求客户端都是支持压缩格式的。我认为不妥,虽然目前主流浏览器都是支持解压的,但是,我们要对接各种源站,请求客户端也很多样,万一客户端不支持解压,就可能出现乱码的情况,作为第三方 WAF,不宜替源站作这种决策。
基于以上,这块保持现状,不启用 gzip。
NGINX 1.9.1 发布版本中引入了一个新的特性 —— 允许套接字端口共享,该特性适用于大部分最新版本的操作系统,其中也包括 DragonFly BSD 和内核 3.9 以后的 Linux 操作系统。套接字端口共享选项允许多个套接字监听同一个绑定的网络地址和端口,这样一来内核就可以将外部的请求连接负载均衡到这些套接字上来。
可以减少多个 Worker 进程接受新连接的竞争,提高多核机器的系统性能。启用后, accept_mutex
配置会失效。
使用示例:
http {
server {
listen 80 reuseport;
server_name localhost;
# ...
}
}
stream {
server {
listen 12345 reuseport;
# ...
}
}
nginx.conf
配置error_log syslog:server=127.0.0.1,facility=local6 debug;`
access_log syslog:server=127.0.0.1,facility=local5 main;
local6
,这个值稍后还将在 syslog-ng
用到 ,更多可选值nginx
,日志数据标记,后面会用来做筛选syslog-ng
端配置为了方便查看日志,这里产生两个日志文件:access.log
记录访问日志;error.log
记录程序日志,包括DEBUG,INFO,ERROR 等。
修改配置文件 /etc/syslog-ng/syslog-ng.conf
,增加如下配置项:
destination d_nginx_err { file("/var/log/nginx/error.log"); };
destination d_nginx_access { file("/var/log/nginx/access.log"); };
filter f_access_nginx { facility(local5); };
filter f_err_nginx { level(debug .. emerg ) and facility(local6); };
log { source(s_src); filter(f_err_nginx); destination(d_nginx_err); };
log { source(s_src); filter(f_access_nginx); destination(d_nginx_access); };
syslog-ng
/etc/init.d/syslog-ng restart
end.
#todo
NSQ是一个基于Go语言的分布式实时消息平台,其当前最新版本是1.0.0版。可用于大规模系统中的实时消息服务,并且每天能够处理数亿级别的消息,其设计目标是为在分布式环境下运
行的去中心化服务提供一个强大的基础架构。NSQ具有分布式、去中心化的拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。
NSQ非常容易配置和部署,且具有最大的灵活性,支持众多消息协议。另外,官方还提供了拆箱即用Go,Python等多种语言库。如果读者兴趣构建自己的客户端的话,还可以参考官方提供的协议规范。
nsqd:一个负责接收、排队、转发消息到客户端的守护进程
-broadcast-address string
注册到 lookupd 的地址,默认是 OS hostname,需要注意的是消费者客户端使用
ConnectToNSQLookupd 时,应该设置服务器真实ip,例如:-broadcast-address=192.168.3.101
否则会连接不到nsqd实例。
-men-queue-size int
内存队列大小,超过该大小后,会将一部分消息存储到磁盘,为了保证消息的不丢失,可将 -men-queue-size=0
那么,节点重启后消息依然存在。
-lookupd-tcp-address value
nsqlookupd 实例运行地址,可以多次设置不同nsqlookupd 实例运行地址。
以上是比较重要的配置项,应该在启动 nsqd 时指定。
nsqlookupd:管理拓扑信息并提供最终一致性的发现服务的守护进程
管理集群下的 nsqd 节点,保证 topic 消息的一致性,一个集群至少布置三个nsqlookupd 实例,保证集群冗余可用性。
-tcp-address string
<addr>:<port> to listen on for TCP clients (default "0.0.0.0:4160")
-http-address string
<addr>:<port> to listen on for HTTP clients (default "0.0.0.0:4161")
nsqadmin:一套Web用户界面,可实时查看集群的统计数据和执行各种各样的管理任务
-lookupd-http-address value
lookupd HTTP address (may be given multiple times)
-notification-http-endpoint string
HTTP endpoint (fully qualified) to which POST notifications of admin actions will be sent
将 web 管理操作消息发送到指定节点
utilities:常见基础功能、数据流处理工具,如nsq_stat、nsq_tail、nsq_to_file、nsq_to_http、nsq_to_nsq、to_nsq
作为高效的分布式消息服务,NSQ实现了合理、智能的权衡,从而使得其能够完全适用于生产环境中,具体内容如下:
ngx_lua
中创建协程的方式主要有三种:
ngx_lua
模块控制,最推荐这些函数创建的协程本质都是 lua coroutine
,但由不同的角色控制(yield
或 resume
),使用场景也不同,使用场景范围最大的是 ngx.timer.at
几乎每个阶段都可以使用,其他两个方法调用限制较多,具体看上面的链接。
前面已经说过,协程是不会主动出让 CPU 时间片的,除非被挂起,阻塞或者代码执行出错。因此,用户创建的协程,如果不主动退出,比如代码进入了死循环,就无法切换执行其他的协程,而且,因为父协程执行的优先级比子协程低,所有请求处理都在同一个主协程内,如果子协程执行时间太长,就会导致 worker 进程请求处理缓慢,甚至卡死,影响整个 OpenResty 处理效率。
另一个问题,当在请求子协程(rewrite, access, content 阶段)内调用 ngx.thread.spawn
或 coroutine.create
创建协程,可能会阻塞当前请求,因为请求子协程是用户创建协程的父协程,必须要所有子协程结束,才能退出,执行下个阶段。ngx.timer.at
创建的协程是与请求协程无关的,可以认为它的父线程是 worker 主协程,所以,当它阻塞时,并不会影响请求处理。
因为协程之间数据是异步的,所以,在协程内使用外部数据要格外小心,最好直接使用参数调用,避免出现非预期的数据变化。
ngx.timer.at
是延迟执行,可以递归实现定时执行的效果,delay
延迟参数不能设置的太小,否则会拖慢当前 worker 的请求处理速度。当需要定时执行的时候,可以直接使用 ngx.timer.every
,更简洁,而且 delay
不能为 0
。
总之,最关键的是与其他语言(例如 Golang)不同, lua 虚拟机没有实现协程管理功能,完全由用户自己控制,使用不当会严重影响程序性能!
1)
package main
import (
"fmt"
"time"
)
const (
total = 100
)
//Printer1
func Printer1(a, b chan int) {
for i := range a {
if i > 100 {
return
}
fmt.Printf("Printer1--%d\r\n", i)
b <- i + 1
}
}
func Printer2(a, b chan int) {
for i := range a {
if i > 100 {
return
}
fmt.Printf("Printer2--%d\r\n", i)
b <- i + 1
}
}
func main() {
a := make(chan int)
b := make(chan int)
go Printer1(a, b)
go Printer2(b, a)
a <- 0
time.Sleep(time.Millisecond * 10) //阻塞
close(a)
close(b)
fmt.Printf("exit\n")
}
2)
package main
import (
"bufio"
"fmt"
"io"
"os"
"sort"
"strings"
)
const (
LOG = "/Users/jinhailang/Desktop/test.txt" //"/home/admin/logs/data.log"
)
func cat(path, key string) ([]string, error) {
var ss []string
fi, err := os.Open(LOG)
if err != nil {
fmt.Printf("Open error: %v\n", err)
return nil, err
}
defer fi.Close()
br := bufio.NewReader(fi)
for {
a, _, c := br.ReadLine()
if c == io.EOF {
break
}
s := string(a)
if strings.Contains(s, key) {
ss = append(ss, s)
}
}
fmt.Println("ss: ", ss)
return ss, nil
}
func uniq(ss []string) map[string]int {
mp := make(map[string]int)
for _, s := range ss {
count, ok := mp[s]
if ok {
count++
mp[s] = count
} else {
mp[s] = 1
}
}
return mp
}
type kv struct {
K string
V int
}
type kvs []kv
func (p kvs) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p kvs) Len() int { return len(p) }
func (p kvs) Less(i, j int) bool { return p[i].V < p[j].V }
func sort_nr(mp map[string]int) []kv {
p := make(kvs, len(mp))
i := 0
for k, v := range mp {
p[i] = kv{k, v}
i = i + 1
}
sort.Sort(p)
return p
}
func main() {
ss, _ := cat(LOG, "alibaba")
sort.Strings(ss)
fmt.Println("sort after ss: ", ss)
mp := uniq(ss)
fmt.Println("uniq after mp: ", mp)
kk := sort_nr(mp)
fmt.Println("sort_nr after kk: ", kk)
fmt.Println("exit.")
}
优势:
不足:
编写数据迁移脚本mRedisMove
,迁移流程:
old redis => mRedisMove => Ohm API => mongodb & new redis
有些动态临时缓存数据,如 带宽控制,不需要存入 mongodb,由API控制
master:10.0.3.42:4500 10.0.3.42:4501
slave:10.0.2.238:4500 10.0.2.238:4501
sentinel:10.0.2.238:26379 10.0.3.42:26379
twemproxy:10.0.3.42:14555
redis-benchmark:10.0.2.238
直接测试 redis :
<iframe src="https://drive.google.com/file/d/0B_bGvu4-BQOCQlkwdWpJVFBjRDA/preview" width="500" height="100"></iframe>测试 twemproxy :
<iframe src="https://drive.google.com/file/d/0B_bGvu4-BQOCUnZBZjhhSW5wbEk/preview" width="500" height="100"></iframe>测试结果相近,官方说法是:twemproxy 性能最多低20%
指针和引用的使用场景关键在于两个字 延后
,当我们需要延后
修改 A
变量,可以先让变量 B
指向 A
,然后操作 B
就是操作 B
了。
以下为伪代码:
B:=&A
B=xxx
但是,golang 在赋值时,需要注意是引用还是拷贝,对于数组,切片,channel,map 默认是引用,即:
mp := make(map[string]string)
mp["a"] = "aaa"
mm := mp
mm["a"] = "sbsb"
mm["b"] = "hahahha"
fmt.Printf("mp: %v", mp) // mp: map[a:sbsb b:hahahha]
但是,当 slice 使用 append
添加元素时需要额外注意,可能踩入一个著名的坑:
mp := make([]string, 1, 1)
mp[0] = "aaa"
mm := &mp
mm = append(*mm, "sbsb")
mm = append(*mm, "hahahha")
fmt.Printf("mp: %v\r\n", mp) // mp: [aaa]
思考上面的代码,为什么 slice 变量 mp
里面值没有被修改呢?
这是因为当使用 append,超过 slice 容量时,会自动重新分配内存大小(扩容),可能是 2 倍或 1/4 的扩容,具体自行研究。这里扩容后 mm
地址会改变,因此 mp
与 mm
指向的地址块也就不一样了。
这种场景下,如何仍然保持 mm
和 mp
指向的地址一致呢?可以使用 引用
,上面的代码作如下修改即可:
mp := make([]string, 1, 1)
mp[0] = "aaa"
mm := &mp
*mm = append(*mm, "sbsb")
*mm = append(*mm, "hahahha")
fmt.Printf("mp: %v\r\n", mp)
合理的使用指针和引用,才能写出更高效,优雅的代码。
思考如下问题:
flag
包是用来解析程序参数输入的,我们可以自定义解析函数,例如:
package main
import (
"flag"
"fmt"
"strings"
)
type ary []string
func (ar *ary) String() string {
return fmt.Sprintf("%v", *ar)
}
func (ar *ary) Set(v string) error {
fmt.Printf("v: %s\r\n", v)
*ar = ary(strings.Split(v, ","))
return nil
}
func aryVar(name, value string, usage string) *ary {
f := ary(strings.Split(value, ","))
flag.CommandLine.Var(&f, name, usage)
return &f
}
func main() {
temp := aryVar("g", "1", "spilt ,")
flag.Parse()
fmt.Printf("temp: %v\r\n", *temp)
}
以上代码中为什么要使用引用,返回指针呢?
这块代码还是包括了接口
的使用,仔细思考,受益颇多。
mshtml.IHTMLDocument2 doc = webBrowser.Document.DomDocument as mshtml.IHTMLDocument2;
mshtml.IHTMLWindow2 win = doc.parentWindow as mshtml.IHTMLWindow2;
win.execScript(@”alert(‘hello webbrowser’)”, “javascript”);
webBrowser.Document.InvokeScript(“test”);
但是,某些情况下- 使用InvokeScript,可能会异常,例如:
string s=webBrowser.Document.InvokeScript(“myobject.test()”).ToString();
可以使用下面的方法解决:
webBrowser.Document.InvokeScript(“eval”,new object[]{“myobject.test()”}).ToString()
HtmlElement head = webBrowser.Document.GetElementsByTagName(“head”)[0];
HtmlElement scriptEl = webBrowser.Document.CreateElement(“script”);
IHTMLScriptElement element = (IHTMLScriptElement)scriptEl.DomElement;
element.text = “function pwdSetSk(sessionKey) { pgeditor.pwdSetSk(sessionKey);return ‘secess’;} function pwdResult() { result=pgeditor.pwdResult();return result;} function machineNetwork() { result=pgeditor.machineNetwork();return result;}”;
head.AppendChild(scriptEl);
string test = webBrowser.Document.InvokeScript(“pwdSetSk”, new object[] { sessionKey }).ToString();
讲的很透彻,把相关知识点都串起来了,值得多看几遍。
这是标题二下的正文
这是标题二下的子标题的正文
这是标题三下的正文
标题要鲜艳
hello world.
实时查看程序运行堆栈信息,收集性能数据,用于程序调优和问题跟踪。非常丰富方便。
1)导入包:_ "net/http/pprof"
2)在 main 函数添加以下代码
func main() {
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
//todo
}
浏览器输入 http://localhost:6060/debug/pprof
,可以查看相关运行信息。
更多功能
用于程序诊断的工具,更直观的展现程序运行过程,定位延迟,并行化和竞争异常代码。
简单实例:
1)
func main() {
trace.Start(os.Stdout)
// todo
trace.Stop()
}
2)数据收集: go run main.go > trace.out
3)查看: go tool trace trace.out
//todo
用来检查在编译阶段没有发现的错误,比如函数调用错误,错误的操作锁或者原子变量,对于写代码比较马虎的人来说,超级好用,可以避免很多细小的问题。当然,这只是一种辅助工具,发现的问题有限。
检查所有项:go tool vet -all ./*.go
输出结果:
jinhailang@jinhailang:~/work/purge$ go tool vet -all ./*.go
./purge_test.go:35: possible formatting directive in Log call
./purge_test.go:40: possible formatting directive in Log call
./purge_test.go:117: Println call ends with newline
./purge_test.go:179: possible formatting directive in Log call
./purge_test.go:180: possible formatting directive in Log call
./purge_test.go:181: possible formatting directive in Log call
./purge_test.go:182: possible formatting directive in Log call
./purge_test.go:185: possible formatting directive in Log call
./purge_test.go:193: possible formatting directive in Log call
数据竞争是并发系统中最常见和最难调试类型的错误之一。特别是在Golang中,由于goroutine的使用,这样的问题更容易出现,好在Golang提供了race这个功能。
go test -race mytest.go
go run -race mytest.go
go build -race mytest.go
go install -race mytest.go
这个参数会引发CPU和内存的使用增加,所以基本是在测试环境使用,不是在正式环境开启。
更多使用
某产品对接最新版 waf proxy(支持 http2.0)后,上传图片超时,其他请求正常。
从 Nginx 的 error 和 access 日志发现,请求转发到 WAF 超时(超过 60 秒),导致客户端主动断开连接(499 状态)。
为了兼容 http2(构建子请求时,将http2 改写成 http1.1),我们使用开源库 resty.http 代替了原来 Openresty 原生的 API ngx.location.capture,性能下降很大,因为 ngx.location.capture
是 Nginx 子请求,本质上不是 http 请求,直接运行在 C 语言级别,效率非常高。当 POST body 数据较大时,这个库转发
的速度会非常慢,从而引起超时。
经过多次对比测试发现,跟 client_body_buffer_size 设置的大小有关,当请求 body 大于 client_body_buffer_size
设置的值时,请求就会变得很慢,甚至超时;反之则正常。
client_body_buffer_size
用来指定 Nginx 读取请求 body 的 buffer 大小值,默认是 8k(32位操作系统)当请求的 body 大于这个值,则会将剩余的字节写入本地文件(磁盘)。所以,根本原因是 resty.http
库转发请求读写磁盘文件效率太低。
可以从两方面进行优化解决:
resty.http
转发的超时时间(httpc:set_timeout(300)
不超过 300 毫秒),因为是转发 WAF 是 旁路
逻辑,可以避免转发 WAF 过慢影响客户正常业务请求。影响就是较大 body(大于 client_body_buffer_size
设置大小) 不会经过 WAF 检测,其实,考虑到性能等问题,WAF 也基本不会处理较大 body (> 1M ) 。client_body_buffer_size
,提高请求 body 读取效率。建议设置 client_body_buffer_size 64k
。resty.http
库源码,看是否能对读写进行优化;问题相关代码片段
access_by_lua_block {
local http = require "resty.http"
local httpc = http.new()
httpc:set_timeout(300)
httpc:set_proxy_options({http_proxy = "http://127.0.0.1:9107"})
ngx.req.read_body()
local req_method = ngx.req.get_method()
local req_body = ngx.req.get_body_data()
local req_headers = ngx.req.get_headers()
local req_url = "http://" .. ngx.var.host .. "/__to_waf__" .. ngx.var.uri
if ngx.var.is_args == "?" then
req_url = req_url .. "?" .. ngx.var.query_string
end
local res, err = httpc:request_uri(req_url, {
version = 1.1,
method = req_method,
body = req_body,
headers = req_headers,
keepalive_timeout = 60,
keepalive_pool = 16,
})
if not res then
ngx.log(ngx.ERR, "failed to request: ", err, ". url: ", req_url)
return ngx.exit(ngx.OK)
end
local status = res.status or 0
local body = res.body
if status == 200 then
if not body or body == "" then
return ngx.exit(ngx.HTTP_CLOSE)
end
elseif status == 403 or status == 400 then
ngx.req.set_method(ngx.HTTP_GET)
return ngx.exec("/__waf_page__/" .. status .. ".html")
end
}
网页截图技术似乎并不是很复杂,网上有很多实例,但是真的想搞清楚的话,还是有很多细节需要注意的。下面是我个人一些经验总结。
有3个技术方案,可以实现IE截图这篇博客说的很详细了,我实现了第2,3种方案,第1个方案缺点太明显了就没做了。
我的实现部分代码
Rectangle body = webBrowser1.Document.Body.ScrollRectangle;
body.Height = height;
body.Width = width;
IntPtr hmemdc = CreateCompatibleDC(hscrdc)
SelectObject(hmemdc, hbitmap);
IViewObject ivo = webBrowser1.Document.DomDocument as IViewObject;
ivo.Draw(1, -1, IntPtr.Zero, IntPtr.Zero,
hscrdc, hmemdc, ref body,
ref body, IntPtr.Zero, 0);
这个方案的优点是可以实现缩放,但是效果很不好,截图比较有点模糊;放大会崩溃,这个问题似乎并没有好解决方案;有些第三方ActiveX没有实现IViewObject接口,就不能显示在截图里面,如银行密码输入控件等。
public static Bitmap GetWindow(IntPtr hWnd,int width,int height)
{
IntPtr hscrdc = GetWindowDC(hWnd);
IntPtr hbitmap = CreateCompatibleBitmap(hscrdc, width, height);
IntPtr hmemdc = CreateCompatibleDC(hscrdc);
SelectObject(hmemdc, hbitmap);
bool re= PrintWindow(hWnd, hmemdc, 0);
Bitmap bmp = null;
if(re)
{
bmp = Bitmap.FromHbitmap(hbitmap);
}
DeleteObject(hbitmap);
DeleteDC(hmemdc);
ReleaseDC(hWnd, hscrdc);
return bmp;
}
这个方案的唯一缺点是不能对特定元素截图,虽然不能直接缩放,但是对得到截图后再进行缩放也是很容易的。不管哪种技术方案,都只能对网页可见区域进行截图,不是完整的网页,为此我调研了很久,也使用了360浏览器的网页截图,也是只能截取可见部分。这个理论上是可以理解的,IE为了性能考虑,只渲染了可见区域的网页,当用户滚动滚动条的时候才会向下渲染。
但是有两个间接完整网页的实现方法:
1)将浏览器设大足够大,一次性显示所有完整网页。
2)滚动滚动条多次截图,再拼接。
使用PrintWindow是最稳定,有效的方法,综合考虑我选择了第三种方案,下面就详细谈谈PrintWindow:
参数详见MSDN,主要就是将窗口绘制成位图,这里我遇到了一个问题:就是当窗口弹出了一个子对话框的时候,是没办法截取到的。可以使用GetWindow(GetParent(vHandle), 6) 取到弹窗的句柄,然后分别截图。
以上就是我的一些经验总结了。
根据近来使用 Nginx 缓存的实践,做一个总结,以及阐述了一般的缓存系统实现的关键和问题。
Nginx 缓存是比较传统的单机本地缓存(需要说明的是,根据 KEY 计算缓存路径的方式是一样的,也就是说多个 Nginx 进程是可以共享相同缓存资源的),比较简单,容易理解。但是,通过 Nginx 缓存的使用与理解,能够窥探到业界通用的缓存系统的设计与实现方法。Nginx 会对上游返回的 Response 进行缓存,存放在特定目录下(磁盘),Nginx 会启动一个专门的 Worker 进程对缓存进行管理,周期性的删除过期缓存等。当一个请求进来,首先会判断是否需要使用缓存(proxy_cache_bypass
),如果需要使用缓存,则直接将请求代理到上游;否则根据 proxy_cache_key
计算出 KEY(32 位 Hash 值),根据这个值从对应的目录(可设置多级目录)下获取相同名称的资源。需要注意的是,磁盘上缓存的是整个响应,包括响应头和响应体,而且,此时,Nginx 不会再对请求头和响应头进行判断,只是直接读进内存,返回响应到客户端,这点也很重要。实例:
Path: /tmp/c/5a/3a1796923cc2b162a5e7f89dc4cf95ac
▒q-\▒▒▒▒▒▒▒▒yq-\▒▒go▒
KEY: httphttpbin.org/cache/60
HTTP/1.1 200 OK
Connection: close
Server: gunicorn/19.9.0
Date: Thu, 03 Jan 2019 02:20:41 GMT
Content-Type: application/json
Content-Length: 219
Cache-Control: public, max-age=60
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.52.1"
},
"origin": "103.126.92.86",
"url": "http://httpbin.org/cache/60"
}
proxy_cache_key
缓存 KEY 的计算是缓存应用中非常关键的部分,一般的自然想到的就是根据请求 URL 来计算,即: $scheme$proxy_host$uri$is_args$args
。
但是,这会导致几个问题:
$scheme$proxy_host$uri
就够了;http_accept_encoding
, cookie
, user_agent
等等)的不同,返回不同的资源,因此计算的时候需要加入这些头部字段,否则会返回错误的资源,可能会导致比较严重的问题;根据标准的 HTTP 协议,服务端(源站)应该在响应头设置 Vary
字段,来显示指定这些影响缓存的头部字段,缓存系统需要支持这种协商机制,对不同的客户端请求返回不同的资源。但是,Vary
这个字段使用的比较少,甚至很多程序员都不清楚这个字段,而且,很多缓存系统也是不支持这个字段。
因此,KEY 的计算需要非常谨慎,最好的办法是根据具体的 Host 选择合适的字段来计算,即
proxy_cache_key
可配置化。一般为了简单快速实现,通用的方式主要有:
http_accept_encoding
一定要加入 KEY,否则会导致将压缩的资源缓存,响应给不接受压缩资源的客户端,出现乱码的情况。虽然现在大部分浏览器都是支持解压的。总之,通用实现的场景下,最重要的是,宁可多缓存几份或;者直接回源,也不能出现请求响应的资源不匹配的情况;
proxy_pass
有些请求响应(大多数动态资源)是不能缓存的,那么 Nginx 怎么会知道哪些响应应该被缓存呢?根据 HTTP 协议, 响应头 Cache-Control
和 Expires
控制缓存失效时间,也即,Nginx 只会缓存这种显式指定了缓存失效时间的响应。
当然,在代理层是可以做很多事情的,比如,可以使用 add_header
来添加 Cache-Control
头,使得源站返回的响应能够被缓存,或者设置 proxy_no_cache
来强制不缓存某些响应。
proxy_cache_purge
缓存系统必不可少的一个功能就是需要支持外部直接刷新的接口,因为很多场景下,需要将被缓存的资源提前失效,这个时候一般是通过 API 接口直接刷新的,也即请求对应的 PURGE
方法。缓存刷新模块看似简单,但是,在大流量的场景下,可能还要支持批量刷新功能,比如大型网站的更新迭代,往往有大量的资源需要更新,CDN 厂商经常会遇到这类需求。要保证缓存能够被快速,准确和稳定的刷新,还是挺有挑战的。一般缓存失效,磁盘上对应的文件并不会立即直接被删除掉,因为读写磁盘代价较大,而且缓存的资源实在太多了,一般先会在内存标记,然后待缓存管理进程(线程)周期性的轮询删除,配置命令 proxy_cache_path
属性 inactive
就是指定删除周期的。
Nginx 中可以使用 proxy_cache_purge
命令来支持 PURGE
请求刷新对应缓存资源。
map $request_method $purge_method {
PURGE 1;
default 0;
}
server {
...
location / {
proxy_pass http://backend;
proxy_cache cache_zone;
proxy_cache_key $uri;
proxy_cache_purge $purge_method;
}
}
但是,这个命令只有商业版才会有,大部分开发者用的应该都是开源版。
This functionality is available as part of our commercial subscription.
在整个架构中,缓存系统往往是比较容易引起问题的模块,除了自身的问题外,因为缓存的使用还有一个与上下游协商的过程,也可能出现外部服务不规范导致的问题,一般比较多的问题已就是缓存未及时刷新,导致客户端使用了旧资源,以及 404 等错误状态未启用缓存(或过滤)导致缓存穿透,缓存刷新,过期不合理,导致缓存雪崩等。一般的使用场景直接使用 Nginx 自带的缓存就够了,但是,对于比较复杂的场景,要自建缓存系统才行,在 CDN 中缓存系统尤为重要。
以下,是我在项目中的使用实例:
proxy_cache_path /tmp levels=1:2 keys_zone=mcache:5m max_size=5g inactive=60m use_temp_path=off;
...
location / {
proxy_cache_key $scheme$proxy_host$uri$is_args$args$http_accept_encoding; # compatible with clients that do not support gzip.
proxy_no_cache $cookie_nocache $arg_nocache$arg_comment;
proxy_no_cache $http_pragma $http_authorization;
proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
proxy_cache_bypass $http_pragma $http_authorization;
proxy_cache_bypass $http_cache_control;
proxy_cache mcahe;
proxy_pass http://test.com;
proxy_set_header Host "myhost.com";
add_header X-Cache $upstream_cache_status;
}
git merge --abort
放弃当前正在进行的合并,深陷冲突无法自拨时使用,可以跳出来重新来过。
git fetch <远程主机名>
一旦远程主机的版本库有了更新(Git术语叫做commit),需要将这些更新取回本地,这时就要用到,对你本地的开发代码没有影响。
git pull <远程主机名> <远程分支名>:<本地分支名>
例如:git pull origin next:master
,取回远程主机某个分支的更新,再与本地的指定分支合并。
git branch -b feature
在本地创建分支feature
并切换到该分支,使用该命令之前应先使用 git fetch
和git pull
,使当前项目到最新版。
git push origin feature/tests
将本地分支feature
推送到远程,分支名为tests
git branch -d feature
删除本地分支feature
,可以使用git push origin /tests
删除远程分支tests
。
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应
用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能
最丰富,最像关系数据库的。
NoSQL一词最早出现于1998年,是Carlo Strozzi开发的一个轻量、开源、不提供
SQL功能的关系数据库。
2009年,Last.fm的Johan Oskarsson发起了一次关于分布式开源数据库的讨论
[2],来自Rackspace的Eric Evans再次提出了NoSQL的概念,这时的NoSQL主要指
非关系型、分布式、不提供ACID的数据库设计模式。
NoSQL,指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不
同于传统的关系型数据库的数据库管理系统的统称。
NoSQL用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿
比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。
在计算机科学中, CAP定理(CAP theorem), 又被称作 布鲁尔定理(Brewer's theorem), 它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
mongod:数据库节点
mkdir -p /data/db
cd ./bin/ && sudo ./mongod
mongo:MongoDB后台管理 Shell
sudo ./mongo
OR
sudo ./mongo 127.0.0.1/admin -supper -p123abc
- 创建新用户
> use m_test
> db.createUser({
user: "mtest",
pwd: "123abc",
roles: [
{ role: "readWrite", db: "admin" },
{ role: "read", db: "test" },
{ role: "read", db: "m_test" },
]
})
注意:roles 定义了操作权限,`验证数据库和权限数据库是分离的`,这里的账号密码可以连接数据库 `m_test`,但连接 `admin`或其他数据库会报验证失败
- 查看当前操作的数据库
> db
test
- 插入一些简单的记录并查找它:
> db.runoob.insert({x:10})
WriteResult({ "nInserted" : 1 })
> db.runoob.find()
{ "_id" : ObjectId("5604ff74a274a611b0c990aa"), "x" : 10 }
MongoDb web:用户界面
MongoDB 提供了简单的 HTTP 用户界面。 如果你想启用该功能,需要在启动的时候指定参数 --rest
./mongod --dbpath=/data/db --rest
MongoDB 的 Web 界面访问端口比服务的端口多1000。默认:http://localhost:28017
常用操作
use DATABASE_NAME 切换/创建数据库
db.dropDatabase() 删除当前数据库
db.collection.drop() 删除集合 collection
show dbs 查看当前所有数据库
show collections 查看当前数据库的所有集合
db.COLLECTION_NAME.insert(document) 插入文档
db.COLLECTION_NAME.find() 查询集合的的所有文档
> db.rundb.find().pretty()
{
"_id" : ObjectId("58ef5da28b88263cdf51ecea"),
"user" : "jin",
"sex" : "man",
"age" : 24
"by" : "sb",
"tags" :[
"a",
"b",
"c"
]
}
与操作
> db.col.find({"sex":"man","age":24}).pretty()
或操作
> db.col.find({$or:[{"sex":"man"},{"age":24}]}).pretty()
条件操作符
示例:db.rundb.find({"age" : {$gt : 18}})
db.COLLECTION_NAME.find().limit(NUMBER) 读取指定数量的数据记录
db.COLLECTION_NAME.ensureIndex({KEY:1}) 建索引,1
:升序 -1
:降序
示例:> db.rundb.ensureIndex({"title":1,"description":-1})
db.COLLECTION_NAME.find().sort({KEY:1}) 排序
aggregate() 聚合(group)
高级操作
MongoDB复制是将数据同步在多个服务器的过程。
复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性。
主节点记录在其上的所有操作oplog,从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。MongoDB的副本集与我们常见的主从有所不同,主从在主机宕机后所有服务将停止,而副本集在主机宕机后,副本会接管主节点成为主节点,不会出现宕机的情况。
MongoDB复制结构图如下所示:
<iframe src="https://drive.google.com/file/d/0B_bGvu4-BQOCdEVDSkZkVHpSUGs/preview" width="640" height="480"></iframe>启动主节点:
mongod --port 27017 --dbpath "./data" --replSet rs0
在客户端操作
启动副本集:
> rs.initiate()
副本集添加成员:
> rs.add(HOST_NAME:PORT)
在Mongodb里面存在另一种集群,就是分片技术,可以满足MongoDB数据量大量增长的需求。
当MongoDB存储海量的数据时,一台机器可能不足以存储数据,也可能不足以提供可接受的读写吞吐量。这时,我们就可以通过在多台机器上分割数据,使得数据库系统能存储和处理更多的数据。
MongoDB中使用分片集群结构分布:
<iframe src="https://drive.google.com/file/d/0B_bGvu4-BQOCZl9uNy03UjBBbE0/preview" width="640" height="480"></iframe>操作步骤:
1)mkdir -p /data/shard/log && mkdir -p /data/shard/s0 && mkdir -p /data/shard/s1 ...
2)./mongod --port 27020 --dbpath=/data/shard/s0 --logpath=/data/shard/log/s0.log --logappend --fork
...
./mongod --port 27023 --dbpath=/data/shard/s1 --logpath=/data/shard/log/s1.log --logappend --fork
3)./mongod --port 27100
4)./mongos --port 40000 --configdb localhost:27100 --fork --logpath=/data/shard/log/route.log --chunkSize 500
* chunk的大小的,单位是MB,默认大小为200MB.
5)使用MongoDB Shell登录到mongos,添加Shard节点
./mongo admin --port 40000
> db.runCommand({ addshard:"localhost:27020" })
> db.runCommand({ enablesharding:"test" }) #设置分片存储的数据库
6)直接按照连接普通的mongo数据库那样,将数据库连接接入接口 40000
在Mongodb中我们使用mongodump命令来备份MongoDB数据。该命令可以导出所有数据到指定目录中。
mongodump命令可以通过参数指定导出的数据量级转存的服务器。
备份:
> mongodump -h dbhost -d dbname -o dbdirectory
示例:> mongodump -h 127.0.0.1:27017 -d test -o /data/dump
恢复:
> mongorestore -h <hostname><:port> -d dbname <path>
RockMongo是PHP5写的一个MongoDB管理工具。
通过 Rockmongo 你可以管理 MongoDB服务,数据库,集合,文档,索引等等。
它提供了非常人性化的操作。类似 phpMyAdmin(PHP开发的MySql管理工具)
Rockmongo 下载地址:http://rockmongo.com/downloads
这几年经历过不少面试,记录下来,偶尔看看,每次体验应该都会不一样吧。
由于时间有限,写的比较仓促,基本满足要求。
Printer1--1
Printer2--2
Printer1--3
Printer2--4
package main
import (
"fmt"
"time"
)
const (
total = 100
)
//Printer1
func Printer1(a, b chan int) {
for i := range a {
if i > 100 {
return
}
fmt.Printf("Printer1--%d\r\n", i)
b <- i + 1
}
}
func Printer2(a, b chan int) {
for i := range a {
if i > 100 {
return
}
fmt.Printf("Printer2--%d\r\n", i)
b <- i + 1
}
}
func main() {
a := make(chan int)
b := make(chan int)
go Printer1(a, b)
go Printer2(b, a)
a <- 0
time.Sleep(time.Millisecond * 10) //阻塞
close(a)
close(b)
fmt.Printf("exit\n")
}
package main
import (
"bufio"
"fmt"
"io"
"os"
"sort"
"strings"
)
const (
LOG = "/Users/jinhailang/Desktop/test.txt" //"/home/admin/logs/data.log"
)
func cat(path, key string) ([]string, error) {
var ss []string
fi, err := os.Open(LOG)
if err != nil {
fmt.Printf("Open error: %v\n", err)
return nil, err
}
defer fi.Close()
br := bufio.NewReader(fi)
for {
a, _, c := br.ReadLine()
if c == io.EOF {
break
}
s := string(a)
if strings.Contains(s, key) {
ss = append(ss, s)
}
}
fmt.Println("ss: ", ss)
return ss, nil
}
func uniq(ss []string) map[string]int {
mp := make(map[string]int)
for _, s := range ss {
count, ok := mp[s]
if ok {
count++
mp[s] = count
} else {
mp[s] = 1
}
}
return mp
}
type kv struct {
K string
V int
}
type kvs []kv
func (p kvs) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p kvs) Len() int { return len(p) }
func (p kvs) Less(i, j int) bool { return p[i].V < p[j].V }
func sort_nr(mp map[string]int) []kv {
p := make(kvs, len(mp))
i := 0
for k, v := range mp {
p[i] = kv{k, v}
i = i + 1
}
sort.Sort(p)
return p
}
func main() {
ss, _ := cat(LOG, "alibaba")
sort.Strings(ss)
fmt.Println("sort after ss: ", ss)
mp := uniq(ss)
fmt.Println("uniq after mp: ", mp)
kk := sort_nr(mp)
fmt.Println("sort_nr after kk: ", kk)
fmt.Println("exit.")
}
TCP 协议两个重要的控制算法,流量控制和拥塞控制算法。流量控制是解决接收端处理不过来的问题,即停车场还可以停多少车的问题;拥塞控制是处理网络链路出问题,数据包发送超时或丢失的问题,即去停车场的路太窄,挤堵了,一次性可以通过多少车(并行发送)的问题。
TCP 中采用滑动窗口来进行传输流量控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
滑动窗口
容许发送方在接收任何应答(ACK)之前可以继续发送的数据包大小。接收方告诉发送方在某一时刻能送多少数据包(称窗口大小),滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。不同的滑动窗口协议窗口大小一般不同。发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧。
拥塞窗口(cwnd):某一源端(发送端)数据流在一个 RTT 内可以最多发送的数据包数。
慢启动阈值(ssthresh):cwnd 超过此阈值则转变控制策略。
拥塞状态
四大算法
拥塞控制主要是四个算法:
- 慢启动算法 Slow Start
所谓慢启动,也就是 TCP 连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序。
慢启动算法:
1) 连接建好的开始先初始化拥塞窗口 cwnd 大小为 1,表明可以传一个 MSS 大小的数据。
2) 每当收到一个 ACK,cwnd 大小加一,呈线性上升。
3) 每当过了一个往返延迟时间 RTT(Round-Trip Time),cwnd 大小直接翻倍,乘以 2,呈指数让升。
4) 还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”(后面会说这个算法)
- 拥塞避免算法 Congestion Avoidance
如同前边说的,当拥塞窗口大小 cwnd 大于等于慢启动阈值 ssthresh 后,就进入拥塞避免算法。
算法如下:
1) 收到一个 ACK,则 cwnd = cwnd + 1 / cwnd
2) 每当过了一个往返延迟时间 RTT,cwnd 大小加一。
过了慢启动阈值后,拥塞避免算法可以避免窗口增长过快导致窗口拥塞,而是缓慢的增加调整到网络的最佳值。
- 拥塞状态时的算法
一般来说,TCP 拥塞控制默认认为网络丢包是由于网络拥塞导致的,所以一般的TCP拥塞控制算法以丢包为网络进入拥塞状态的信号。对于丢包有两种判定方式,一种是超时重传 RTO[Retransmission Timeout] 超时,另一个是收到三个重复确认 ACK。
超时重传是 TCP 协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
但是如果发送端接收到3个以上的重复ACK,TCP就意识到数据发生丢失,需要重传。这个机制不需要等到重传定时器超时,所以叫做快速重传,而快速重传后没有使用慢启动算法,而是拥塞避免算法,所以这又叫做快速恢复算法。
超时重传 RTO[Retransmission Timeout] 超时,TCP 会重传数据包。TCP 认为这种情况比较糟糕,反应也比较强烈:
由于发生丢包,将慢启动阈值 ssthresh 设置为当前 cwnd 的一半,即 ssthresh = cwnd / 2。cwnd重置为 1。
进入慢启动过程:最为早期的TCP Tahoe算法就使用上述处理办法,但是由于一丢包就一切重来,导致cwnd重置为1,十分不利于网络数据的稳定传递。所以,TCP Reno 算法进行了优化。当收到三个重复确认 ACK 时,TCP开启快速重传 Fast Retransmit 算法,而不用等到 RTO 超时再进行重传:
1)cwnd 大小缩小为当前的一半
2)ssthresh 设置为缩小后的cwnd大小
3)然后进入快速恢复算法 Fast Recovery
- 快速恢复算法 Fast Recovery
TCP Tahoe 是早期的算法,所以没有快速恢复算法,而 Reno 算法有。在进入快速恢复之前,cwnd 和ssthresh 已经被更改为原有 cwnd 的一半。快速恢复算法的逻辑如下:
cwnd = cwnd + 3 * MS // 加 3 * MSS 的原因是因为收到3个重复的ACK
重传 DACKs 指定的数据包。如果再收到 DACKs,那么 cwnd 大小增加一。
如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法。将 cwnd 设置为 ssthresh,然后进入拥塞避免算法。
系统执行流程(概述)
Nginx master
Nginx 启动后,首先进入 main()
函数,加载初始化配置信息(nginx.conf),调用所有模块的 init_module
方法,然后再调用 ngx_master_process_cycle
。
Nginx worker
如上图,函数 ngx_start_worker_processes
会循环 fork()
出配置项 worker_processes
指定的 worker 进程。关于系统函数 fork
,需要知道的是:
Linux下一个进程在内存里有三部分的数据,就是"代码段"、"堆栈段"和"数据段"。其实学过汇编语言的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。
函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝:
* 子进程和父进程使用相同的代码段;
* 子进程复制父进程的堆栈段和数据段;
如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?
一般CPU都是以"页"为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,象INTEL的CPU,其一页在通常情况下是 4086字节大小,而无论是数据段还是堆栈段都是由许多"页"构成的,fork函数复制这两个段,只是"逻辑"上的,并非"物理"上的。
也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的" 页"从物理上也分开。
系统在空间上的开销就可以达到最小。这种技术又叫 [Copy-on-write (COW)](https://en.wikipedia.org/wiki/Copy-on-write
)
因此,worker 进程的 lua vm,是 fork
时,从 master 进程拷贝而来,因为是拷贝,在 worker 进程内的操作,自然就不会影响 master 堆栈(lua vm)变化。
PS:nginx 进程间通信,主要使用三种方式:
hup
等;shmget
等操作;openresty 的 ngx.shared.DICT 就是基于系统共享内存实现的。
lua_nginx_module 模块的函数 ngx_http_lua_init
也会在 main
调用所有模块的 init_module
方法时被调用,该函数调用 ngx_http_lua_init_vm
创建 lua 虚拟机实例,大致流程如下:
main -> ngx_http_lua_init -> ngx_http_lua_init_vm -> ngx_http_lua_new_state (创建虚拟机实例)
ngx_http_lua_new_state 主要功能
1)生成新 vm,lua_State
2)设置默认的package路径,路径由编译脚本生成
3)ngx_http_lua_init_registry() 初始化 lua registry table。registry 中保存了多个 lua 运行期需要保持的变量,例如:cache 的 lua 代码,协程的引用地址等,这些变量如果放在 lua 堆栈中会被 GC 机制自动回收,所以需要另外保存。
4)ngx_http_lua_init_globals() 初始化 global 全局变量。ngx 的各种 api 和内置变量就是在这里由 ngx_http_lua_inject_ngx_api() 进行注入,提供给 lua 脚本调用。
虚拟机创建完成后,继续调用 init_handle
函数,该函数将加载执行配置项 init_by_lua*
对应的 lua 代码文件。即 init_by_lua* lua 代码是加载在 master 虚拟机的。
nginx 分为 11 个执行阶段:
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0, // 接收到完整的HTTP头部后处理的阶段
NGX_HTTP_SERVER_REWRITE_PHASE, // URI与location匹配前,修改URI的阶段,用于重定向
NGX_HTTP_FIND_CONFIG_PHASE, // 根据URI寻找匹配的location块配置项
NGX_HTTP_REWRITE_PHASE, // 上一阶段找到location块后再修改URI
NGX_HTTP_POST_REWRITE_PHASE, // 防止重写URL后导致的死循环
NGX_HTTP_PREACCESS_PHASE, // 下一阶段之前的准备
NGX_HTTP_ACCESS_PHASE, // 让HTTP模块判断是否允许这个请求进入Nginx服务器
NGX_HTTP_POST_ACCESS_PHASE, // 向用户发送拒绝服务的错误码,用来响应上一阶段的拒绝
NGX_HTTP_TRY_FILES_PHASE, // 为访问静态文件资源而设置
NGX_HTTP_CONTENT_PHASE, // 处理HTTP请求内容的阶段,大部分HTTP模块介入这个阶段
NGX_HTTP_LOG_PHASE // 处理完请求后的日志记录阶段
} ngx_http_phases;
lua_nginx_module 模块在其中的 rewrite, access, content,log 阶段注册了 handler 函数。这几个阶段执行流程如下:
上图,除了 init_by_lua*
都是在 worker 进程内处理的,模块初始化阶段(ngx_http_lua_init
),将这些阶段处理函数挂载到对应的阶段处理函数数组,以 content_by_lua
为例,大致流程如下:
ngx_http_lua_content_by_lua(设置 handle file 路径) -> ngx_http_lua_content_handle -> ngx_http_lua_content_handle_file(加载 lua 代码) -> ngx_http_lua_content_by_chunk -> ngx_http_lua_by_thread(在协程里面执行)
所有协程共享 woker 进程的 lua vm,每个外部请求都由一个 lua 协程处理,协程之间数据隔离,即不同请求间的数据隔离。
ngx_lua 协程
RFC 3986 指定了保留字符,分为两类,如下:
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
可能只会选择对其中的部分保留字符编码,而不同的编程语言和函数选择编码的字符不一样,这就导致不同系统之间的解析出来的 http url 不一致。
以 Go 为例,运行下面两段代码。
代码 1:
murl := "http://testwhc5.b0.upaiyun.com/anything/<Wake Up To Dream> What's Media Lab 2016.mp4"
mu, _ := url.Parse(murl)
fmt.Printf("url: %s\r\n", murl)
fmt.Printf("url.string: %s\r\n", mu.String())
输出:
url: http://testwhc5.b0.upaiyun.com/anything/<Wake Up To Dream> What's Media Lab 2016.mp4
url.string: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What%27s%20Media%20Lab%202016.mp4
代码 2:
murl = "http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4"
mu, _ = url.Parse(murl)
fmt.Printf("url: %s\r\n", murl)
fmt.Printf("url.string: %s\r\n", mu.String())
输出:
url: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4
url.string: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4
运行以上两段代码,会发现输出的 url string 有微小的差异,上面的将 '
转码成了 %27
,而下面没有转码。
按理来说,调用同样的函数,编码后的 url 应该一样才对,问题出在哪儿呢?
查看函数源码发现,函数 url.String
输出的是编码后的 url 字符串。但是,如果输入的原始 url 已经是编码的,那么就不会做处理直接使用原始 url。判断函数 validEncodedPath
代码片段:
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@':
// ok
case '[', ']':
// ok - not specified in RFC 3986 but left alone by modern browsers
case '%':
// ok - percent encoded, will decode
default:
if shouldEscape(s[i], encodePath) {
return false
}
那么,上面两段代码输出结果不一致的原因基本找到了,那就是因为 validEncodedPath
判断认为代码 2 输入的 url 是已经编码过(已被 ngx_lua 编码)的,就将原始字符串输出了。而代码 1 输入的 url 被 Go 函数 EscapedPath
编码之后再输出。
再进一步查看 Go 的编码函数 escape(s string, mode encoding)
,判断是否需要编码函数 shouldEscape(c byte, mode encoding)
判断,代码片段如下:
case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
// Different sections of the URL allow a few of
// the reserved characters to appear unescaped.
switch mode {
case encodePath: // §3.3
// The RFC allows : @ & = + $ but saves / ; , for assigning
// meaning to individual path segments. This package
// only manipulates the path as a whole, so we allow those
// last three as well. That leaves only ? to escape.
return c == '?'
...
可以看到 Go 会对 url path 中 '
, ?
等特殊字符编码,而不会对 ;
, =
等字符编码。当然更多不同语言的编码差异, 需要具体分析了。对于使用多种编程语言的复杂系统,需要特别注意这点,最好是最后处理 url 的程序自己保持编码一致,即将输入的 url,处理成符合自己的编码规范。因为,一般解码函数(如 Go)没有这种差异,所以可以先对输入的 url 解码,再编码,这样能保证编码的 url 字符串,符合本程序语言规范的,从而保证本程序最终的 url 编码字符串一致
所以,最好是不要在 url 中使用这些保留字符。不同的语言会有微小的差异,很容易导致 bug,查找和处理都很麻烦。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.