0%

背景

我们想要从Nginx接受请求开始,生成一个Unique Tracing ID,不仅记录在Nginx的日志中,也要贯穿到整个后台的服务,从而利用这个ID方便问题的排查。

方案一

利用Nginx丰富的内置变量,拼接出一个“unique enough id”。这里使用了五个变量:

  • $pid: Nginx worker process id
  • $msec: timestamp in millisecond
  • $remote_addr: client address
  • $connection: TCP connection serial number
  • $connection_requests: current number of requests made through a connection

实现步骤

1.在nginx.conf的location模块里:

1
2
3
4
5
location / {
proxy_pass http://upstream;
set $req_id $pid.$msec.$remote_addr.$connection.$connection_requests;
proxy_set_header X-Request-Id $req_id;
}

2.在http模块的 log_format 里加上 $req_id,至此Nginx的日志中将包含这个ID

1
log_format trace '... $req_id';

3.在后台服务中可以通过下面的方式获取$req_id

1
2
3
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write(self.request.headers["X-Request-Id"])

4.重启Nginx

1
nginx -s reload

问题

格式混乱,信息冗余,生成的效果如下:

1
97372.1493211301.686.127.0.0.1.471.32

方案二

使用Nginx内置的变量 $request_id
这是最直接的办法,使用Nginx自带的一个$request_id,一个16位比特的随机数,用32位的16进制数表示。

1
proxy_set_header X-Request-Id $request_id;

问题

这Nginx 1.11.0 版本新增加的feature,使用Nginx旧版本,或者依赖某些二次开发的Nginx版本,例如 Tengine 继承的是Nginx 1.8.1 版本,都面临着升级Nginx的问题。

方案三

使用 Lua 生成一个uuid.
利用Lua轻量小巧的特性,嵌入到Nginx的配置文件当中,然后生成一个uuid.

实现步骤

1.在 http 模块里加入:

1
2
3
4
5
6
7
8
map $host $uuid {
default '';
}
lua_package_path '/path/to/uuid4.lua';
init_by_lua '
uuid4 = require "uuid4"
math = require "math"
';

2.在server模块里加入:

1
2
3
set_by_lua $uuid '
return uuid4.getUUID()
';

3.在location模块里加入:

1
proxy_set_header X-Request-Id $uuid;

4.uuid4.lua
引用自 第三方库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
--[[
The MIT License (MIT)
Copyright (c) 2012 Toby Jennings
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--]]

local M = {}
-----
math.randomseed( os.time() )
math.random()
-----
local function num2bs(num)
local _mod = math.fmod or math.mod
local _floor = math.floor
--
local result = ""
if(num == 0) then return "0" end
while(num > 0) do
result = _mod(num,2) .. result
num = _floor(num*0.5)
end
return result
end
--
local function bs2num(num)
local _sub = string.sub
local index, result = 0, 0
if(num == "0") then return 0; end
for p=#num,1,-1 do
local this_val = _sub( num, p,p )
if this_val == "1" then
result = result + ( 2^index )
end
index=index+1
end
return result
end
--
local function padbits(num,bits)
if #num == bits then return num end
if #num > bits then print("too many bits") end
local pad = bits - #num
for i=1,pad do
num = "0" .. num
end
return num
end
--
local function getUUID()
local _rnd = math.random
local _fmt = string.format
--
_rnd()
--
local time_low_a = _rnd(0, 65535)
local time_low_b = _rnd(0, 65535)
--
local time_mid = _rnd(0, 65535)
--
local time_hi = _rnd(0, 4095 )
time_hi = padbits( num2bs(time_hi), 12 )
local time_hi_and_version = bs2num( "0100" .. time_hi )
--
local clock_seq_hi_res = _rnd(0,63)
clock_seq_hi_res = padbits( num2bs(clock_seq_hi_res), 6 )
clock_seq_hi_res = "10" .. clock_seq_hi_res
--
local clock_seq_low = _rnd(0,255)
clock_seq_low = padbits( num2bs(clock_seq_low), 8 )
--
local clock_seq = bs2num(clock_seq_hi_res .. clock_seq_low)
--
local node = {}
for i=1,6 do
node[i] = _rnd(0,255)
end
--
local guid = ""
guid = guid .. padbits(_fmt("%X",time_low_a), 4)
guid = guid .. padbits(_fmt("%X",time_low_b), 4)
guid = guid .. padbits(_fmt("%X",time_mid), 4)
guid = guid .. padbits(_fmt("%X",time_hi_and_version), 4)
guid = guid .. padbits(_fmt("%X",clock_seq), 4)
--
for i=1,6 do
guid = guid .. padbits(_fmt("%X",node[i]), 2)
end
--
return guid
end
--
M.getUUID = getUUID
return M

问题

Lua的这个模块太长,担心性能问题,需要进行性能评估。

方案四

还是利用Lua脚本,使用时间戳加随机数的方式
关键步骤:

1
2
3
set_by_lua $rdm_number '
return os.time() .. os.clock()*100 .. math.random(1000000000, os.time())
';

问题

os.time()的精确度在1秒,os.clock()的精确度在0.01秒,这样处理之后,总的精度在10毫秒,没有达到要求。
Lua有一个 Luasocket 模块,可以达到毫秒级别的精度,但是需要安装。

方案五

结合Nginx的 $msec 变量和 Lua 的随机数
关键配置

1
2
3
4
5
6
7
8
9
10
11
server {
...
set_by_lua $rdm_number '
return math.random(1000000000, os.time())
';
location / {
...
set $req_id $msec$rdm_number;
proxy_set_header X-Request-Id $req_id;
}
}

终记

最终确定方案五,简单,方便,影响最小。
在方案选择、测试过程中,还遇到了环境搭建相关的问题,将记录在下篇文章中,敬请期待!


参考

1.http://stackoverflow.com/questions/17748735/setting-a-trace-id-in-nginx-load-balancer
2.https://blog.ryandlane.com/2014/12/11/using-lua-in-nginx-for-unique-request-ids-and-millisecond-times-in-logs/
3.http://www.jb51.net/article/82167.htm
4.http://nginx.org/en/docs/http/ngx_http_core_module.html#.24args
5.http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_id

目录

  • 1.解决方案
  • 2.原始文件和最终生成效果
  • 3.pom.xml 中插件添加
  • 4.html中 css/js 文件引用规则

1.解决方案

1
2
3
4
5
6
7
解决问题:
防止浏览器缓存,修改静态文件(js/css)后无效,需要强刷。

解决方案:
使用 maven 的 com.google.code.maven-replacer-plugin 插件,
在项目打包 package 时自动为静态文件追加 xxx.js?v=time 的后缀,
从而解决浏览器修改后浏览器缓存问题,此插件只会在生成 war 包源码时生效,不需要修改任何代码。

2.原始文件和最终生成效果

1
2
3
4
5
原始文件:
<script src="${resource!}/js/xxx/xxx.js"></script>

打包后:
<script src="${resource!}/js/xxx/xxx.js?v=20180316082543"></script>

3.pom.xml 中插件添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<properties>
<!-- maven.build.timestamp 默认时间戳格式 -->
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
</properties>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<!-- 使用缓存 -->
<useCache>true</useCache>
</configuration>
<executions>
<!-- 在打包之前执行,打包后包含已经执行后的文件 -->
<execution>
<id>prepare-war</id>
<phase>prepare-package</phase>
<goals>
<goal>exploded</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.3</version>
<executions>
<!-- 打包前进行替换 -->
<execution>
<phase>prepare-package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 自动识别到项目target文件夹 -->
<basedir>${build.directory}</basedir>
<!-- 替换的文件所在目录规则 -->
<includes>
<include>${build.finalName}/WEB-INF/views/*.html</include>
<include>${build.finalName}/WEB-INF/views/**/*.html</include>
</includes>
<replacements>
<!-- 更改规则,在css/js文件末尾追加?v=时间戳,反斜杠表示字符转义 -->
<replacement>
<token>\.css\"</token>
<value>.css?v=${maven.build.timestamp}\"</value>
</replacement>
<replacement>
<token>\.css\'</token>
<value>.css?v=${maven.build.timestamp}\'</value>
</replacement>
<replacement>
<token>\.js\"</token>
<value>.js?v=${maven.build.timestamp}\"</value>
</replacement>
<replacement>
<token>\.js\'</token>
<value>.js?v=${maven.build.timestamp}\'</value>
</replacement>
</replacements>
</configuration>
</plugin>
</plugins>

4.html中 css/js 文件引用规则

文件引用结尾处,必须是pom.xml文件中添加的规则:

1
2
3
<script src="${resource!}/js/xxx/xxx.js" type="text/javascript"></script>

<link href="${resource!}/css/xxx/xxx.css" rel="stylesheet" type="text/css">

  1. 点击菜单栏左下角“开始”按钮,在右侧的菜单栏中点击“管理工具”选项,在弹出的右侧菜单栏中点击“事件查看器”选项。
image-20210514132656150
  1. 弹出的“事件查看器”界面右侧,左侧有“windows日志”选项,单击左侧“+”。

    image-20210514132203607
  2. 在展开的列表中选中“系统”这一选项可以看到系统产生的日志记录。

    image-20210514132225205
  3. 为了方便直接查找重启的日志,在右侧的菜单栏中点击“筛选当前日志…”。

    image-20210514132241330
  4. 在弹出的窗口中选择“筛选器”选项卡,并在“任务类别”这一选项框上方的框中输入“1074”的事件ID,接着点击下方的“确定”即可看到系统所有的重启日志记录。(注:“1074”为系统重启的事件ID)

    image-20210514132301214 image-20210514132317909
  5. 若要查看详细信息,双击该条目即可。

    image-20210514132335162

服务器

工具类

软件 地址 账号
FTP 101.68.85.251:18027(公网)
ftp.jhkj.com:18027(内网)
jh_ftp_read / YTkzODI2YmUyZDI(只读)
jh_ftp_write / NzU1OWJlM2VkMmJ(读写)

数据库分库分表

前言

公司最近在搞服务分离,数据切分方面的东西,因为单张包裹表的数据量实在是太大,并且还在以每天60W的量增长。 之前了解过数据库的分库分表,读过几篇博文,但就只知道个模糊概念, 而且现在回想起来什么都是模模糊糊的。

今天看了一下午的数据库分库分表,看了很多文章,现在做个总结,“摘抄”下来。(但更期待后期的实操) 会从以下几个方面说起:

第一部分:实际网站发展过程中面临的问题。

第二部分:有哪几种切分方式,垂直和水平的区别和适用面。

第三部分:目前市面有的一些开源产品,技术,它们的优缺点是什么。

第四部分:可能是最重要的,为什么不建议水平分库分表!?这能让你能在规划前期谨慎的对待,规避掉切分造成的问题。

名词解释

库:database;表:table;分库分表:sharding

数据库架构演变

刚开始我们只用单机数据库就够了,随后面对越来越多的请求,我们将数据库的写操作和读操作进行分离, 使用多个从库副本(Slaver Replication)负责读,使用主库(Master)负责写, 从库从主库同步更新数据,保持数据一致。架构上就是数据库主从同步。 从库可以水平扩展,所以更多的读请求不成问题。

但是当用户量级上来后,写请求越来越多,该怎么办?加一个Master是不能解决问题的, 因为数据要保存一致性,写操作需要2个master之间同步,相当于是重复了,而且更加复杂。

这时就需要用到分库分表(sharding),对写操作进行切分。

分库分表前的问题

任何问题都是太大或者太小的问题,我们这里面对的数据量太大的问题。

用户请求量太大

因为单服务器TPS,内存,IO都是有限的。 解决方法:分散请求到多个服务器上; 其实用户请求和执行一个sql查询是本质是一样的,都是请求一个资源,只是用户请求还会经过网关,路由,http服务器等。

单库太大

单个数据库处理能力有限;单库所在服务器上磁盘空间不足;单库上操作的IO瓶颈 解决方法:切分成更多更小的库

单表太大

CRUD都成问题;索引膨胀,查询超时 解决方法:切分成多个数据集更小的表。

分库分表的方式方法

一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。 我们从面临的问题,开始解决,阐述: 首先是用户请求量太大,我们就堆机器搞定(这不是本文重点)。

然后是单个库太大,这时我们要看是因为表多而导致数据多,还是因为单张表里面的数据多。 如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。

如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。 分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。

垂直拆分

  1. 垂直分表

    也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。

  2. 垂直分库

    垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。

数据库业务层面的拆分,和服务的“治理”,“降级”机制类似,也能对不同业务的数据分别的进行管理,维护,监控,扩展等。 数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。 数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。

水平拆分

  1. 水平分表

    针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。

  2. 水平分库分表

    将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。

  3. 水平分库分表切分规则

    1. RANGE

      从0到10000一个表,10001到20000一个表;

    2. HASH取模

      一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。

    3. 地理区域

      比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。

    4. 时间

      按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。

分库分表后面临的问题

事务支持

分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

多库结果集合并(group by,order by)
TODO

跨库join

TODO 分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 粗略的解决方法: 全局表:基础数据,所有库都拷贝一份。 字段冗余:这样有些字段就不用join去查询了。 系统层组装:分别查询出所有,然后组装起来,较复杂。

分库分表方案产品

目前市面上的分库分表中间件相对较多,其中基于代理方式的有MySQL Proxy和Amoeba, 基于Hibernate框架的是Hibernate Shards,基于jdbc的有当当sharding-jdbc, 基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding, 通过重写spring的ibatis template类的Cobar Client。

还有一些大公司的开源产品:
image-20210423201850225