简栈文化

Java技术人的成长之路~

背景

前段时间做了很多慢SQL的优化工作,这周刚好又被反馈服务出现了死锁导致了业务报错。看了一下云数据库的告警日志,发现出现了比较多的事务未提交死锁等待行锁的严重警告。都是一些棘手的运维工作,涉及到业务流程的梳理、SQL的优化等工作。

今天趁这个机会,我们一起看下如何去分析这些问题,主要看下等待行锁、死锁。

数据库有哪几种锁?

每次说数据库锁,感觉一大堆。其实如果按照一定的纬度去整理下,还是比较清晰的。如图:

MySQL锁

力度划分:表锁、页锁、行锁

算法划分:Record LockGap LockNext-key Lock

实现机制:乐观锁、悲观锁

兼容性:排它锁、共享锁、意向锁

每次说锁,其实也要跟它的隔离级别挂钩才行,其实都是为了去实现某一个功能才产生的。所以不可以一概而论,总之记住几个大的背景:

  • 不同的隔离级别才会有不同的锁,比如RR才会出现Gap Lock,因为要避免幻读的问题,所有要把它相邻的数据也要锁住。
  • 锁是作用在索引上的,包含:聚簇索引、非聚簇索引

如何看日志?

1
2
3
4
SET GLOBAL innodb_status_output=ON; -- 开启输出
SET GLOBAL innodb_status_output_locks=ON; -- 开启锁信息输出

SHOW ENGINE INNODB STATUS\G

通过SHOW ENGINE INNODB STATUS可以看到锁的一些信息

先确定一下自己数据库的隔离级别信息,我现在数据库的版本是8.0.26

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果是比较老的数据库
select @@tx_isolation;
select @@global.tx_isolation;

ERROR 1193 (HY000): Unknown system variable 'tx_isolation' 如果报错,说明你的数据库比较新,需要采用新的查询语句。

mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.26 |
+-----------+
1 row in set (0.01 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+

select @@session.transaction_isolation;

查看锁的情况

1
2
3
4
5
6
# 老版本
select * from information_schema.innodb_locks;

# 高版本
SELECT ENGINE_LOCK_ID, THREAD_ID, OBJECT_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA
FROM performance_schema.data_locks;

等待行锁

这个问题也会比较常见,如果出现一个事务获取了锁,如果它不释放或者提交的话,那么后面的人就一直获取不到锁。如果获取锁的时间过长的话,后面的流程就会一直卡住。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 建表
CREATE TABLE `id_test_rr` (
`pk` int NOT NULL,
`id` int DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`pk`),
KEY `idx_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- 准备数据
INSERT INTO id_test_rr values(1, 1, 'a');
INSERT INTO id_test_rr values(2, 3, 'b');
INSERT INTO id_test_rr values(3, 5, 'c');
INSERT INTO id_test_rr values(4, 7, 'c');
INSERT INTO id_test_rr values(5, 5, 'b');

事务1:

1
2
3
4
5
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM id_test_rr WHERE id = 5;
Query OK, 2 rows affected (0.01 sec)

这个时候不要提交事务,看一下现在加锁的情况。

image-20211204213254443

  • 5,3 加了一把X锁
  • 5,5加了一把X锁
  • 3 主键加了一个X锁
  • 5主键加了一个X锁
  • 7,4加了GAP Lock,X,这里因为是RR,为防止幻读需要加GAP来保证。

事务2:

1
2
3
mysql> update id_test_rr set name = 'x' where id = 5;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

image-20211204213956251

当事务1提交的时候,事务2马上就获取到了锁。

image-20211204214236092

如何设置超时时间:

1
2
3
4
5
6
show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
|Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout| 50 |
+--------------------------+-------+

死锁

死锁产生的条件

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

其实,产生死锁的条件无非就是这4个条件,其实大学里学习操作系统的时候就有学习到过。解决死锁,也只需要让它们只要有一个条件不满足就可以了。

死锁的过程分析

建表语句

1
2
CREATE TABLE t (i INT) ENGINE = InnoDB;
INSERT INTO t (i) VALUES(1);

事务1:

1
2
3
4
5
6
7
8
9
10
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE;
+------+
| i |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

image-20211204215103399

事务2:

1
2
3
4
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

DELETE FROM t WHERE i = 1;

image-20211204215206747

然后事务1继续进行删除操作

1
mysql> DELETE FROM t WHERE i = 1;

image-20211204215251156

事务2报错了:Deadlock found when trying to get lock;

死锁产生了!因为事务1需要锁X来删除行,而事务2拿着锁X并正在等待事务1释放锁S。看看2个事务的状态:

  • 事务1: 拿着锁S,等待着事务2释放锁X
  • 事务2: 拿着锁X,等待着事务1释放锁S

image-20211204215342270

参考地址

背景

我们来先看一个图,了解一下故()事()的背景:

http://static.cyblogs.com/Jietu20211113-162059.jpg

2个跑批任务,其实做的事情是同一件事情,都是为了跟下游系统保持数据的一致性。大任务是每个2h跑一次,小任务是每隔10mins跑一次。除了这2个定时任务以外,还有一个额外的监控任务来做类似的对账,如果发现出现对账不平,就会出现邮件/短信告警到相关的责任上。

这是一个非常有特点的定时任务跑批任务+监控告警的场景了。

从上面的场景上看,我们可以得出一些结论:为了保证一致性写了大小Job来保证,并且还给出了监控告警,说明数据的重要性是比较强的。

某天,出现了频繁的告警提示,每10分钟就告警一次,而且内容没有发生变化,说明同步的index没有变化过。

错误排查

任务有在正常的执行吗?

第一反应肯定是在思考,我的大任务与小任务都有正常执行吗?因为之前的都是正常的。上午看了一下日志与进程发现有在跑,还跑除了多次任务,日志打印不明确,看不到具体分支的逻辑。总结一下问题点:

  • 任务很忌讳出现上一个任务没有跑完,下一个任务又继续开启一个新的任务,给服务器带来了不少的压力。一般如果对接了好的分布式调度能力,基本也很容易客服这个问题。
  • 关键分支日志打印不明确,导致定位很难

先修复上面2个问题,短时间对接一个新的分布式调度时间上不可能,只能简单的改shell脚本让其不执行。

1
2
3
4
5
6
7
8
9
d=`date`

count=`ps -ef |grep {jobKeyword} |grep -v grep | wc -l`
if [ $count -lt 1 ]; then
echo "$d : do {jobKeyword} . " >> /data/{projectName}/sync.log
python xxxxx.py
else
echo "$d : {jobKeyword} no finished, this time do nothing. " >> /data/{projectName}/sync.log
fi

然后在重点的地方添加上日志,其实这些操作都是一些非常简单,但是可以带来明显效果的步骤。反正,我基本都是如此的去做的,你什么信息都拿不到,你根本无法入手。

部署上去后,发现每次在insert into一条数据的时候,日志就卡住了,结合代码确定,确定就是insert into的时候,数据库没有返回,而其他的表以及其他数据的都是可以正常操作的。

出现了LOCK WAIT

第一反应,就是看下这条SQL现在是一个什么状态?我们可以利用SHOW PROCESSLIST看下

1
2
3
select * from information_schema.PROCESSLIST t;

SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ;

然后就发现该SQL语句的trx_state=LOCK WAIT,那说明没有获取到锁。那我们具体如何推断是谁没有释放锁了?

1
2
3
4
5
6
7
8
# 第1步:
SELECT * FROM information_schema.innodb_locks ;

# 第2步:1,2没有特别的先后顺序,之后为了确定trx_requested_lock_id以及是谁获取了锁lock_trx_id + local_data
SELECT * FROM information_schema.innodb_trx t where t.trx_state = 'LOCK WAIT';

# 第3步:找到对应的执行语句
select * from information_schema.PROCESSLIST t where t.id = {lock_trx_id}

可惜,那条语句已经是sleep的状态了,无法看到具体的SQL。在这里可以推断,就是有一条SQL在对数据{local_data}操作的时候获取了一把锁,但是因为事务未提交,导致后面的SQL再对{local_data}操作的时候要获取锁,无法获取到。理论上获取不到锁,一会儿也会释放掉报错出来。通过查询innodb_lock_wait_timeout=7200,默认值应该是50

解决掉问题

到这一步就很明确了,就是让未提交事务的SQL结束掉,或者提交掉。此时只有kill掉这个进程的选项了。执行:

1
kill {lock_trx_id};

再执行就立马发现数据没有了,获取到了锁。

1
SELECT * FROM information_schema.innodb_trx t where t.trx_state = 'LOCK WAIT';
总结一下
  • innodb_lock_wait_timeout 设置不合理,时间太久了
  • 出现获取不到锁的场景,需要告警到邮件、手机上来。
  • 大任务与小任务的时间要搓开,出现这种情况也是对同一行数据进行X操作并且未释放锁导致的。把事务的时间搞短一点。可以每次都去获取连接,也不要一次连接执行很长时间。

实验性操作

就直接看脚本好了

http://static.cyblogs.com/Jietu20211113-171928.jpg

当右边的事务对同一条数据进行X操作的时候,它是要获取锁的。

1
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

这个时候可以去看下锁的表

1
2
3
4
5
6
7
8
select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
|lock_id | lock_trx_id | lock_mode| lock_type | lock_table | lock_index| lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
|757082:3279:3:2 | 757082 | X | RECORD | `test`.`test` | PRIMARY | 3279 | 3 | 2 | 1 |
|757081:3279:3:2 | 757081 | X | RECORD | `test`.`test` | PRIMARY | 3279 | 3 | 2 | 1 |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rowsin set, 1 warning (0.00 sec)

查看一下设置的超时时间

1
2
3
4
5
6
show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
|Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout| 50 |
+--------------------------+-------+

看关于事务的描述

1
show engine innodb status

查看当前的事务

1
2
3
4
5
6
7
8
9
10
mysql> show processlist;
+----+-----------------+-----------+--------------------+---------+--------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+--------------------+---------+--------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 286110 | Waiting on empty queue | NULL |
| 8 | root | localhost | test | Sleep | 956 | | NULL |
| 9 | root | localhost | test | Sleep | 754 | | NULL |
| 10 | root | localhost | information_schema | Query | 0 | init | show processlist |
+----+-----------------+-----------+--------------------+---------+--------+------------------------+------------------+
4 rows in set (0.01 sec)

参考地址

前言

现在对于一个开发来说,Docker应该是再熟悉不过了。还记得在20132014左右的时候,听说多最多的就是Cloud Foundry,那个时候就一直在说云的事情。后面Docker就绝杀了它

那它帮我们解决了一个什么问题了?面试的时候也许会问到。

在很久以前,我们开发代码,估计最蛋疼的事情就是发布版本了。我还记得在房多多的时候(2014~2016)左右,每次发布几个开发围绕在运维的身边,有时候运维忙不过来,开发就直接在运维的电脑上开始VIM干活了,修改若干配置。由于多环境的原因,我们无法保证每个环境都是一样的。

  • 可能你的操作系统不同,导致打包、发布的脚本不同
  • 环境不同,没有很好的配置管理,你的代码有不同的写法
  • 特别是跟操作系统相关的那些参数,可能瞬间就会带来性能问题

那么Docker就可以把我们的操作系统、代码、脚本等都一起打包成一个Image,就可以保证只要是运行同一个Image,我们的所有内容都是一样的。就不会出现,我在测试环境跑的好好的,一到生产连启动都成问题。

问题

现在一般一个POD就只跑一个进程,DevOps会根据我们的发布流水线自动的将一个项目进行打包、发布,整套的CI、CD做的是行云流水。但是,每个项目ROOT下都会需要一个叫Dockerfile的文件。但偏偏有一些历史项目,没有Dockerfile文件,只有一个Run的容器再跑,真的是非常惊悚。docker rm [OPTIONS] CONTAINER [CONTAINER...],就GAME OVER了。

怎么办?

方法1:以当前容器作为基础镜像

真的,什么也不想。先保个底,把你当前的容器打包成一个镜像推送到仓库里去,哪天有以外或者说需要基于它做一些事情的时候才有可能。比如:你要本地也部署一份代码来debug

一般都是私有的仓库,会需要输入用户名与密码

1
2
3
4
➜  ~ docker login {仓库地址}
Username: chenyuan
Password:
Login Succeeded

然后,将镜像打包推送到私有仓库去

1
2
3
4
5
docker commit -a "name" -m "小陈来拯救你" 706e502e8693 {镜像地址}:{tag}

docker push {镜像地址}:{tag}

docker pull {镜像地址}:{tag}

但是这样子的问题在于,我们无法知道环境依赖了哪些模块,如果需要重新再部署一套,我为了保证环境的干净又需要删除哪些东西。就是无法知道增加与减少哪些东西,也就会导致环境存在不一致性,失去了我们的初衷。

方法2:从运行的容器中复制

先把镜像跑起来,然后从运行起来的容器中复制文件出来,复制命令如下:

1
2
3
# 从容器复制文件或目录到宿主机器
docker cp 6619ff360cce:/opt/h2-data/pkslow ./
docker cp 6619ff360cce:/opt/h2-data/pkslow/pkslow.txt ./

第一种方法并不是万能的,因为有些镜像过于简单,少了许多基础命令,以至于无法复制文件,也无法进入shell环境。其次,要运行起来再操作,也有点占用资源,比较麻烦。

方法3:解压镜像tar文件(推荐)

此方法就是相当于反编译,拿到当时打镜像时候你做的详细操作。比较麻烦,但是是最靠谱的,最具有操作性的。

先将镜像保存为tar文件,命令如下:

1
docker save -o {name}.tar {镜像地址}:{tag}

下载后就会有一个tar包在本地,然后就解压出来。可以看一下manifest.json文件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[
{
"Config": "dca33100e3683d6fb4d56a4c142ccccc1c113f061454a64bc07c852fe068ea1d.json",
"RepoTags":
[
"{镜像地址}:{tag}"
],
"Layers":
[
"216168069a5195a9424b3a73a62bda39e4d5f8dcae2f7149a336c2e29beeb06b/layer.tar",
"4b0e1f4bede4cef5dee11aff78ff89f543dc62eb02306db1b96d896b101e069d/layer.tar",
"3fe7f20416fdd4958cc18b6fb0d28881147246c32677d102a431c31bf12288f7/layer.tar",
"84c1758c9c15f83d8aa4e1ad13c2918aea80f802f01d19eeb2f7c6e1897d7160/layer.tar",
"31bf0d828ecc19f178d8337e1c22a030984e9185e805b48ea911bd866730af2f/layer.tar",
"7b30e9a6f195343744ca82c66d31b61771e8d6502a271ad60deb1fa1103e83ca/layer.tar",
"522ee848bbd06c6e4dad8d5200b83c9197ccce717fb09687b435190d287f6829/layer.tar",
"a64965663d7c30ed09d35f05439dcfb6247f030df0d72a0e78f54fb6ae5a8c74/layer.tar",
"a93f0f89669c097497a3e3de7aeffebeba2838f180e4f13844be55fe124885ae/layer.tar",
"fd69896888f7361654ed0e27ed2634311b6707dd20706487e33e24f32bb23ebe/layer.tar",
"69c55c418aba5b8fb5239b4e8b092e02100f4ec49dae8ded9cc0a161b21884d7/layer.tar",
"5ef51ffa437403d5d33a40208c3781ea84a93f53947e5d7fad086092667bd3b1/layer.tar"
]
}
]

图片是解压后的效果,里面都会存在一个layer.tar,这里再解压就是当时打镜像时候的一些资源文件。

http://static.cyblogs.com/image-20210928200049751.png

http://static.cyblogs.com/image-20210928200334841.png

红色的部分就是我们想要的内容。再辛苦一点,把自己想要的东西整理出来。描述的比较轻描淡写,任何事情只要手动去做一遍,就会理解与记住。

参考地址

背景

之前在思考双活/多活架构的时候,其实对于蓝绿发布是有一些了解的,也梳理过在底层存储是一份,服务是多份的模式有做过深入的分析。但那个时候对于Kubernetes的了解还不是很熟悉,是通过传统的方式来考量的。

因为现在的互联网公司基本都是上云了,我们也必须对于Kubernetes那一整套要有比较深入、熟悉的运用才能真的提高我们的效率。先聊一下,我为什么需要利用灰度+蓝绿发布的模式来去做?

现在有一个比较老的项目,应该在10年+,每天请求量大概在1.5亿+,峰值的QPS在6000/s,存在着比较多性能问题。现在需要在它上面新增一个服务,为了后面优化做准备,比如:请求的分流、限流、熔断、日志的上报与监控(新)、统一编译处理,特殊报文转换等。也就是说,只要你新增加了一层,你才有可能更好的去做更多的事情。

那么我们需要达到一些什么的基础条件了?

  • 服务流量比较大,我们需要对新服务的可靠性需要验证,需要灰度先了解
  • 因为存在慢查询,不能在滚动发布中,导致请求还未执行完毕,就被k8s kill掉了,业务会感知到502

如果是你?针对于这2个基础的要求,你会如何去思考的你架构方案呢?

思考

新增服务的思考:

  • 它的性能必须要强、服务稳定。一个服务的性能好不好,其实跟它的:I/O模型、线程模型、数据结构、算法等息息相关。比如:你在思考Redis单线程为什么快的时候?应该就很能get到这里的点了。解决这个问题,我们选择了Go语言来开发(当然,最熟悉的语言风险最小),为了保证性能,也是做了2轮非常细致的压测。
  • 发布过程中不能因为kill掉服务导致请求502。如果说我在发布的过程中,我把滚动这一步省略掉,直接先准备好一份最新的,验证可以后,我一刀直接把流量引导最新服务上,老的服务也不会断掉,这是否就可以达到效果了?

方案设计

下面是我画的一个架构图,方便大家的理解,一共是3条路线:

http://static.cyblogs.com/如何利用k8s的label与ingress做蓝绿发布_架构图02.jpg

路线1:原始的路线
  • IngressService:server-readStatefulSet:server-g3-read + server-g3-read-gray,整条链路是通过ingress的指向与selector的标签:k8s-app:server-read
路线2:灰度方案
服务正常

就是我只能让一少部分的流量进入到新的服务(2%~10%,支持慢慢调整,其实就是pod的数量占比)。

  • 2%的概率走的路径:IngressService:server-gateway-read01StatefulSet:server-gateway-read01注册中心获取负载地址Service:server-readStatefulSet:server-g3-read + server-g3-read-gray,整条链路是通过ingress的指向与selector的标签:server-app:server-gateway-read01
  • 98%的概率还是走的路线1
服务异常解决方案
  • 因为这个流量是通过节点数来控制的,如果发生异常,可以把灰度节点的POD数量调整为0
  • 还可以从ingress的地址切换到线路1的原始方案。这一招永远生效,因为一整套label标签依然存在。
路线3:蓝绿路线
服务正常
  • 100%的流量全部走灰度方案的。即:IngressService:server-gateway-read01StatefulSet:server-gateway-read01注册中心获取负载地址Service:server-readStatefulSet:server-g3-read + server-g3-read-gray。但是它的selector的标签:server-app:server-gateway-read02
服务异常解决方案
  • 直接切换ingress地址到线路1或者是线路2都可以

最终的方案

后面如果长期稳定后,方案2其实就没有必要再继续灰度了,直接就替换成线路3了。相当于是一个蓝绿+主备的模式了。优缺点非常的明显:

  • 优点:解决了重启中可能出现的中断问题,其实也可以通过一些 Graceful Shutdown优化。
  • 缺点:就是发布的一瞬间,你是需要多出一倍的机器来支撑服务的。

再温馨提示一下,因为做了蓝绿发布,我们的系统对应的配置中心应该也最好是要分开的。系统之间要避免蓝色通过与绿色通道之间的交叉访问等问题。

我们在新做项目的时候,需要对我们的服务有有一些性能指标,比如:SLA(需要达到多少个9)、QPSTPS等。因为这些量化的数字让我们更加的了解我们的系统。

我们如何压测?其实个人觉得有2种场景。

第一种:是我们明确的知道目标,看我们通过大量的并发看我们是否有达到。如果没有达到,我们需要通过水平扩容、性能优化等让其达到。

第二种:是我们不知道目标,通过压测可以知道一个固定配置下的单机单服务的最大性能,让我们对它有一个彻底的认识。为后面的目标做更多的铺垫与准备,或者跟行业水平对比,看看差距有多少。

如何用wrk进行压测?

Github地址:https://github.com/wg/wrk,该项目也是开源项目,关注的人还不少,有30.4K。咨询了一下身边的同事,使用它的人还不少。主要的语言的是C语言。

http://static.cyblogs.com/Jietu20211023-153923.jpg

安装
1
2
3
4
5
6
git clone https://github.com/wg/wrk

make

-- 拷贝wrk到bin
cp wrk /usr/sbin/wrk
压测脚本

压测脚本press.sh

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
bf=0i
rm ./report_mock.${1}.txt 2> /dev/null
for k in 10; do
bf=`expr ${k} \* 100`
for len in 512k; do
echo "start length ${len}" >> ./report_mock.${1}.txt
./wrk -c${bf} -t16 -d3m --timeout 2m --latency -s ./post_${len}.lua http://${1}/press/${len} >> ./report_mock.${1}.txt
echo "----------------------------------------------------" >> ./report_mock.${1}.txt
done
sleep 10
done

Lua脚本post_512.lua

1
2
3
4
wrk.method = "POST"
wrk.body = '{"key":"value"}'
wrk.headers["Content-Type"] = "application/json"
wrk.headers["X-Forwarded-For"] = "6.6.6.6"

这里是一个通用脚本,大致的含义是:

  • 删除掉原来生成的文件
  • 循环10次,其实也就是从并发数从100~1000依次进行压测
  • 返回的报文大小控制在512K,这个可以为根据你的Request去匹配你的Response。
  • -t16:启动16个线程
  • -d3m:也测时间是3mins
  • –timeout:2m 超时时间是2mins
  • post_${len}.lua:就是构造512K返回的Request参数对应的lua脚本
  • report_mock.${1}.txt:结果会append到该文件中去
  • ${1}:就是你要向哪个服务器发起请求的host+port

总结一下,这些时间需要根据自己的服务器性能去调整,有可能压测出来的数据就是空的,因为超时了未返回Response。

参数解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
使用方法: wrk <选项> <被测HTTP服务的URL>                            
Options:
-c, --connections <N> 跟服务器建立并保持的TCP连接数量
-d, --duration <T> 压测时间
-t, --threads <N> 使用多少个线程进行压测

-s, --script <S> 指定Lua脚本路径
-H, --header <H> 为每一个HTTP请求添加HTTP头
--latency 在压测结束后,打印延迟统计信息
--timeout <T> 超时时间
-v, --version 打印正在使用的wrk的详细版本信息

<N>代表数字参数,支持国际单位 (1k, 1M, 1G)
<T>代表时间参数,支持时间单位 (2s, 2m, 2h)
执行的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
start length 512k
Running 3m test @ http://{ip+port}/press/512k # {ip+port}是你自己的
16 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 145.17ms 942.97ms 0.86m 98.34%
Req/Sec 1.39k 114.27 3.58k 70.07%
Latency Distribution
50% 36.58ms
75% 48.78ms
90% 185.51ms
99% 1.78s
3994727 requests in 3.00m, 65.25GB read
Non-2xx or 3xx responses: 3863495
Requests/sec: 22181.33
Transfer/sec: 371.00MB

会给一个分布非常的好:50%、75%、90%、99%。

但是如果说这么看大量的数据不够直观,这里再提供一个一个python脚本来解析里面的值。使其能把这些日志的重要的信息提取出来:

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
import math

def ms(fr):
fr = fr.lower()
if fr[-2:] == 'ms':
to = float(fr[:-2])
elif fr[-1:] == 's':
to = float(fr[:-1]) * 1000
elif fr[-1] == 'm':
to = float(fr[:-1]) * 1000 * 60
return to

def mb(fr):
fr = fr.lower()
if fr[-2:] == 'gb':
to = float(fr[:-2]) * 1000
elif fr[-2:] == 'mb':
to = float(fr[:-2])
elif fr[-2:] == 'kb':
to = float(fr[:-2]) / 1000
elif fr[-1] == 'b':
to = float(fr[:-1]) / 1000 / 1000
elif fr[-1] == 'k':
to = float(fr[:-1]) / 1000
elif fr[-1] == 'm':
to = float(fr[:-1])
return to

def parse_one(one):
ret = {}
for l in one.split('\n'):
if l.find('test @ http://') > 0:
ret['host'] = l.split('://')[1].split('/')[0].strip()
elif l.find('start length') == 0:
ret['size'] = l.split(' ')[-1].strip()
elif l.find('threads and') > 0:
ret['threads'] = int(l.split('threads and')[0].strip())
ret['conns'] = int(l.split('threads and')[1].split('connections')[0].strip())
elif l.find(' Latency') == 0:
ret['l_avg'] = ms(l[len(' Latency'):].lstrip().split(' ')[0])
elif l.find(' 50%') == 0:
ret['l_50'] = ms(l[len(' 50%'):].strip())
elif l.find(' 90%') == 0:
ret['l_90'] = ms(l[len(' 90%'):].strip())
elif l.find(' 99%') == 0:
ret['l_99'] = ms(l[len(' 99%'):].strip())
elif l.find('Requests/sec:') == 0:
ret['qps'] = float(l[len('Requests/sec:'):].strip())
elif l.find('Transfer/sec:') == 0:
ret['mbps'] = mb(l[len('Transfer/sec:'):].strip())
return ret

with open('/Users/chenyuan/Desktop/report_mock.127.0.0.1.txt') as f:
all = f.read().strip()
out = []
for one in all.split('----------------------------------------------------\n'):
r = parse_one(one)
out.append((r['host'], r['size'], r['conns'], r['l_avg'], r['l_50'], r['l_90'], r['l_99'], r['qps'], r['mbps']))
out.sort()
for o in out:
print('\t'.join([str(i) for i in o]))

最终你可以再在文本里面利用列操作的方式,将内容归整到Excel中去。你就可以很好的汇报与分享给他人了~

http://static.cyblogs.com/Jietu20211023-160325.jpg

如何利用Jmeter进行压测?

简介

Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。

Apache jmeter 可以用于对静态的和动态的资源(文件,Servlet,Perl脚本,java 对象,数据库和查询,FTP服务器等等)的性能进行测试。它可以用于对服务器、网络或对象模拟繁重的负载来测试它们的强度或分析不同压力类型下的整体性能。你可以使用它做性能的图形分析或在大并发负载测试你的服务器/脚本/对象。

Jmeter也是在进行压测中使用场景很多的软件,图形界面操作起来非常的友好。简单的写一个Demo流程出来。

下载安装

官网:http://jmeter.apache.org/download_jmeter.cgi

http://static.cyblogs.com/Jietu20211023-160918.jpg

下载解压完毕后大概的一个目录结构,可以把bin配置到path中就可以直接通过jmeter密令激活软件了。

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
➜  jmeter pwd
/Users/chenyuan/Tools/jmeter
➜ jmeter ll
total 64
-rw-rw-r--@ 1 chenyuan staff 15K 1 2 1970 LICENSE
-rw-rw-r--@ 1 chenyuan staff 167B 1 2 1970 NOTICE
-rw-rw-r--@ 1 chenyuan staff 9.6K 1 2 1970 README.md
drwxrwxr-x@ 43 chenyuan staff 1.3K 1 2 1970 bin
drwxr-xr-x@ 6 chenyuan staff 192B 1 2 1970 docs
drwxrwxr-x@ 22 chenyuan staff 704B 1 2 1970 extras
drwxrwxr-x@ 104 chenyuan staff 3.3K 1 2 1970 lib
drwxrwxr-x@ 104 chenyuan staff 3.3K 1 2 1970 licenses
drwxr-xr-x@ 19 chenyuan staff 608B 1 2 1970 printable_docs

# 我这里直接运行,最好配置PATH,后续更加的方便
➜ jmeter ./bin/jmeter
================================================================================
Don't use GUI mode for load testing !, only for Test creation and Test debugging.
For load testing, use CLI Mode (was NON GUI):
jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]
& increase Java Heap to meet your test requirements:
Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the jmeter batch file
Check : https://jmeter.apache.org/usermanual/best-practices.html
================================================================================
# 就这代表已经启动了,运行期间不要终端命令框。
创建测试

先看一张整体的图吧,对它有一个比较整体的认识:

http://static.cyblogs.com/Jietu20211023-161956.jpg

我们可以按照https://www.cnblogs.com/stulzq/p/8971531.html 的步骤一步的去做。我这里就不做太多重复的介绍。因为相对比wrk来的简单。

最后可以点击运行来跑单测,一般我们会调整线程数的大小、发送的频率来进行压测看结果。

http://static.cyblogs.com/Jietu20211023-162302.jpg

我们在断言的地方是可以做很多的事情的,因为什么样的结果是正确的,什么样的结果是失败的。获取需要从Response中、Header里截取一些关键的key与value来做逻辑。这些可以通过编写脚本来去做到,那就算相对高阶一点的操作了。后续可以继续深入一下~

最后可以在汇总报告里可以看出来我们的一个性能情况,SLA的比例等。

作为一名后端开发,对自己写出来的服务进行一个非常全面的性能压测是很有必要的。对于系统的一个QPS、TPS、SLA这些数字应该随口就能说出来。哪些地方存在性能瓶颈?然后再去找相应的方案去优化掉。很多时候,性能可能就会是一个最大的风险,它会导致我们的服务整体的瘫痪、不可用。这些很有可能就跟我们的KPI、奖金挂钩~

参考地址

背景

项目中开始用Go,最近写了一下Demo,发现语法还是非常好用,大部分比Java还是简洁很多,也有一些很细节的约定。比如:

  • 字母大小写控制是全包还是本包内访问
  • 变量定义了就一定要使用,否则就会编译不通过等等

更好的就是方法可以返回多个值,这个跟Java比较就是减少很多的封装。因为Go的线程模型特点,用来写一些需要高并发、高性能的项目还是非常好的。所以,趁这个机会也好好的深入了解下。现在也是把PythonPHPGo等都学习一遍,每种语言都有它的优缺点,其实都还挺不错的。

针对于Go语言里:&*的区别,什么时候该用什么做一个总结。

指针

我们经常会听到别人说Go是值传递,某某某是引用传递,某某某是指针传递,等等各种各样的说法。

那么首先他们的区别是什么呢?什么是指针?指针其实也是一个变量,只不过这个变量里面存的不是intfloatstruct,而是一个地址address,然后在这个address上所存储的数据可以通过指针来被阅读到。

OK,指针变量存储的是一个地址,地址从哪里来的?那就得问一个变量的地址怎么取得呢?在变量前面加上一个&符号就行。

好的,指针变量存储了这个地址了,那这个地址所存储的值怎么被阅读到呢?也就是指针所指向的值怎么拿到呢?在指针变量前面加上一个*符号就行。

怎么修改指针所指向的数据呢?在前面加上*符号之后再赋一个新的值就可以了。

我们来看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
a := "vernonchen"
b := &a

fmt.Println("a的值", a)
fmt.Println("a的地址", b)
fmt.Println("b的值", b)
fmt.Println("b的指针指向", *b)

*b = "not vernonchen"
fmt.Println("修改后,b的指针指向", *b)
}

执行输出的结果是

1
2
3
4
5
a的值 vernonchen
a的地址 0xc00008e1e0 // 使用的是b
b的值 0xc00008e1e0
b的指针指向 vernonchen //使用的是指针
修改后,b的指针指向 not vernonchen // 重新指向新的值

指针的作用

为什么要有指针这个东西?它有什么关键性的作用呢?

我们来看下面这段代码:

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
package main

import "fmt"

type User struct {
userName string
}

func modify(user User) {
user.userName = "vernonchen2222"
}

func modifyWithPoint(user *User) {
user.userName = "vernonchen3333"
}

func main() {

user := User{userName: "vernonchen"}

fmt.Println("userName = ", user.userName)

modify(user)

fmt.Println("userName = ", user.userName)

modifyWithPoint(&user)

fmt.Println("userName = ", user.userName)
}

执行完后,输出的结果:

1
2
3
userName =  vernonchen
userName = vernonchen // 没有被替换
userName = vernonchen3333 // 被替换

所以指针的作用:

  1. 指针不但可以帮助函数内部修改外部变量的值,还可以帮助我们在任何地方修改其所指向数据的值;

  2. 传递指针参数可以节省拷贝大结构体的内存开销;

总结

  • 指针也是变量,只不过存储的是地址
  • 通过指针可以去修改其指向数据的值
  • 指针可以帮助我们在任何地方修改其所指向数据的值
  • 传递指针参数可以节省拷贝大结构体的内存开销

参考地址

背景

如果说你的数据量并发量不大,或者你的数据量很少没有到千万级别,也许pt-oscgh-osc,online-ddl这些工具都用不着。但是,如果你的数据量很大,数据又很热。如果你没有这些工具,你可能无法完成对一个数据库新增一个字段或者任何一个简单的DDL语句。

简单的分析一下,为了保证数据一致性问题,我们在哪儿都会遇到锁的问题,锁是用来保证顺序性的。谁先拥有锁,谁就可以先执行。锁也会存在力度问题,它跟你要做的一件事情息息相关,我们也会在性能上去做取舍,所有就好了行锁、表锁等。

Waiting for table metadata lock

说一下我遇到的这个场景,数据量数据大概在800W左右,但是表非常的热,长事务也很多。当我要对一个表新增字段的时候,这个时候如果你经验不够足,可能就会“量成大祸”。一般在做DDL会出现:Waiting for table metadata lock。

如果长时间获取不到锁的话,就出现一个可怕的情况:

  • 如果前面的事务未提交,当前是获取不到锁,就不可以执行DDL语句
  • DDL语句未执行之前,后面的请求全部是被hold住的

这样子就会导致一前一后同时夹击,导致整个业务不可用。那么出现Waiting for table metadata lock可能是由哪些原因导致的?

场景一:长事物运行,阻塞DDL,继而阻塞所有同表的后续操作

通过show processlist可以看到TableA上有正在进行的操作(包括读),此时alter table语句无法获取到metadata 独占锁,会进行等待。

这是最基本的一种情形,这个和mysql 5.6中的online ddl并不冲突。一般alter table的操作过程中,在after create步骤会获取metadata 独占锁,当进行到altering table的过程时(通常是最花时间的步骤),对该表的读写都可以正常进行,这就是online ddl的表现,并不会像之前在整个alter table过程中阻塞写入。(当然,也并不是所有类型的alter操作都能online的,具体可以参见官方手册:http://dev.mysql.com/doc/refman/5.6/en/innodb-create-index-overview.html)
处理方法: kill 掉 DDL所在的session.

1
2
3
4
5
6
# 找出所有执行时间超过 5 分钟的线程,拼凑出 kill 语句,方便后面查杀
select concat('kill ', id, ';')
from information_schema.processlist
where Command != 'Sleep' AND COMMAND != 'Binlog Dump GTID' AND COMMAND != 'Binlog Dump'
and Time > 300
order by Time desc;
场景二:未提交事物,阻塞DDL,继而阻塞所有同表的后续操作

通过show processlist看不到TableA上有任何操作,但实际上存在有未提交的事务,可以在 information_schema.innodb_trx中查看到。在事务没有完成之前,TableA上的锁不会释放,alter table同样获取不到metadata的独占锁。

处理方法:通过 select * from information_schema.innodb_trx\G, 找到未提交事物的sid, 然后 kill 掉,让其回滚。

场景三:显示事务失败未提交

通过show processlist看不到TableA上有任何操作,在information_schema.innodb_trx中也没有任何进行中的事务。这很可能是因为在一个显式的事务中,对TableA进行了一个失败的操作(比如查询了一个不存在的字段),这时事务没有开始,但是失败语句获取到的锁依然有效,没有释放。从performance_schema.events_statements_current表中可以查到失败的语句。

官方手册上对此的说明如下:

If the server acquires metadata locks for a statement that is syntactically valid but fails during execution, it does not release the locks early. Lock release is still deferred to the end of the transaction because the failed statement is written to the binary log and the locks protect log consistency.

也就是说除了语法错误,其他错误语句获取到的锁在这个事务提交或回滚之前,仍然不会释放掉。because the failed statement is written to the binary log and the locks protect log consistency 但是解释这一行为的原因很难理解,因为错误的语句根本不会被记录到二进制日志。

**处理方法:**通过performance_schema.events_statements_current找到其sid, kill 掉该session. 也可以 killDDL所在的session.

上述是手动操作,毕竟是一个比手速的过程,而且也不能保证保证100%,还需要经常的kill进程才行。

gt-osc使用

请看参考地址,对于一些online ddl工具研究后,对gt-osc做了一个初步的使用。

1
2
3
4
5
6
pt-online-schema-change --host=xxx -uxxx -pxxx \
--alter "add xxx int(1) NOT NULL DEFAULT '0' COMMENT 'xxx', add xxx varchar(10) NOT NULL DEFAULT '0' COMMENT 'xxx' , lock=none" D=xxx,t='xxx' \
--execute \
--print \
--statistics \
--no-check-alter

执行后其实它做了这些工作,具体如下:

  1. 相关环境参数检查
  2. 检查该表格是否存在
  3. show create table xxx
  4. create table _xxx_new
  5. alter table _xxx_new
  6. 创建删除触发器 pt_osc_db_table_name_del
  7. 创建更新触发器 pt_osc_db_table_name_upd
  8. 创建插入触发器 pt_osc_db_table_name_ins
  9. 按块拷贝数据到新表,拷贝过程对数据行持有S锁
  10. analyze 新表
  11. rename 表名,RENAME TABLE db.table_name TO db._table_name_old, db._table_name_new TO db.table_name
  12. 删除旧表
  13. 删除新表上的删除、更新、插入 触发器

可以去看一下网站的说明:https://www.percona.com/blog/2019/06/07/how-pt-online-schema-change-handles-foreign-keys/

http://static.cyblogs.com/Jietu20211018-000822.jpg

参考地址

代码分支管理

大家在做微服务拆分后,难免会导致Application项目以及一些二房包的数量加剧,10+个项目我想应该是很容易的超过。然后这些细粒度的拆分后就会导致发布版本时候的麻烦。

展示一下现阶段我们的一个git的分支流程图,仅供参考。

http://static.cyblogs.com/git_flow_publish_20210415.jpg

简单说明一下:

  • dev环境每次都是从master拉取一个分支,取名为dev-{发布日期}-{sequenceId}sequenceId01开始叠加,避免一个版本需要反复拉取多次。
  • 到了test阶段,也是为了收敛(前期严格一点)。提测的时候代码需要合并到test分支来。
  • UAT阶段,还是需要从master拉取分支,如果出现要重新拉取分支的情况下,还是严格拉取master分支的代码。主要是为了与master保持一致,避免把别人的覆盖掉。取名为release-{发布日期}-{sequenceId},规则同上。
  • 上生产后,验证完毕后需要把代码合并到master分支,并且打包tag

但是在这个过程中,需要有拉取新分支,合并分支,批量删除分支,打tag等等繁琐的操作,项目一多如果有一个批量脚本就更好了。下面我就列举一些平时使用最多的几个,仅供参考:

批量脚本

01-批量拉取新分支

比如一个新的迭代要开始了,就需要从master拉取dev分支。

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
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 过滤目录
EXCLUDE_DIR_OR_FILE=("cr-test-demo" "git_batch_co_brach.sh" "git_batch_co_push_remote.sh" "git_batch_co_merge_push_remote.sh")

# 分支名字
OLD_BR_NAME="$1"
NEW_BR_NAME="$2"

# 校验参数
if [ -f $OLD_BR_NAME ]; then
echo "Please input your old branch name."
exit 0
fi

if [ -f $NEW_BR_NAME ]; then
echo "Please input your new branch name."
exit 0
fi

# 切换到某个分支
function gitCheckoutBrachAndPushRemote() {
echo "-------- $1 --------"
cd "$1"
pwd
git checkout $OLD_BR_NAME
git fetch
git pull
git checkout -b $NEW_BR_NAME $OLD_BR_NAME
git push --set-upstream origin $NEW_BR_NAME
echo "success push branch " $NEW_BR_NAME " to remote"
}

## get all folder in "$WORK_DIR"
for i in `ls "$WORK_DIR"`;do
cd "$WORK_DIR"
if [[ -d $i ]] && [[ ! "${EXCLUDE_DIR_OR_FILE[@]}" =~ "${i}" ]];then
gitCheckoutBrachAndPushRemote "$i"
fi
done

具体使用,比如以master为基础拉取一个新分支

1
./git_batch_co_push_remote.sh master dev-20210419-01
02-批量切换分支

做不同的需求时候,需要来回的切换分支。

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
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 过滤目录
EXCLUDE_DIR_OR_FILE=("cr-test-demo" "git_batch_co_brach.sh" "git_batch_co_push_remote.sh" "git_batch_co_merge_push_remote.sh")

# 分支名字
BR_NAME="$1"

# 校验参数
if [ -f $BR_NAME ]; then
echo "Please input your branch name."
exit 0
fi

# 切换到某个分支
function gitCheckoutBrach() {
cd "$1"
pwd
git fetch
git checkout $BR_NAME
echo "success checkout to " $BR_NAME
}

## get all folder in "$WORK_DIR"
for i in `ls "$WORK_DIR"`;do
cd "$WORK_DIR"
if [[ -d $i ]] && [[ ! "${EXCLUDE_DIR_OR_FILE[@]}" =~ "${i}" ]];then
gitCheckoutBrach "$i"
fi
done

具体的使用,先切换成master分支。

1
./git_batch_co_brach.sh dev-20210412-01 
03-批量删除远程分支

有时候不小心写错了分支名字,或者分支太多,需要删除批量删除远程分支

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
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 过滤目录
EXCLUDE_DIR_OR_FILE=("cr-test-demo" "git_batch_co_brach.sh" "git_batch_co_push_remote.sh" "git_batch_co_merge_push_remote.sh")

# 分支名字
BR_NAME="$1"

# 校验参数
if [ -f $BR_NAME ]; then
echo "Please input your branch name."
exit 0
fi

# 切换到某个分支
function doSomething() {
cd "$1"
pwd
git fetch
git push origin --delete $BR_NAME
echo "success delete remote branch: " $BR_NAME
}

## get all folder in "$WORK_DIR"
for i in `ls "$WORK_DIR"`;do
cd "$WORK_DIR"
if [[ -d $i ]] && [[ ! "${EXCLUDE_DIR_OR_FILE[@]}" =~ "${i}" ]];then
doSomething "$i"
fi
done

具体的使用

1
./git_batch_delete_remote.sh dev-20210412-01

结果就是可以把远程分支dev-20210412-01一次性全部删除掉。

04-批量合并分支代码

从一个环境到另外也跟环境,就需要大量的merge操作。其实Gitmerge操作比SVN的要好很多。

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
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 过滤目录
EXCLUDE_DIR_OR_FILE=("cr-test-demo" "git_batch_co_brach.sh" "git_batch_co_push_remote.sh" "git_batch_co_merge_push_remote.sh")

# 分支名字
BR_NAME="$1"

# 校验参数
if [ -f $BR_NAME ]; then
echo "Please input your branch name."
exit 0
fi

# 切换到某个分支
function gitCheckoutBrach() {
cd "$1"
pwd
git merge $BR_NAME
git push
echo "success merge from branch" $BR_NAME " and push remote"
}

## get all folder in "$WORK_DIR"
for i in `ls "$WORK_DIR"`;do
cd "$WORK_DIR"
if [[ -d $i ]] && [[ ! "${EXCLUDE_DIR_OR_FILE[@]}" =~ "${i}" ]];then
gitCheckoutBrach "$i"
fi
done

具体使用,记得要先更新相关的代码哦~

比如是从dev合并到test,那么先更新对应的代码,然后co到test的目录去。

1
./git_batch_co_merge_push_remote.sh dev-20210412-01
05-批量给分支打tag

比如我们的流程是在上生产后为master打一个新的tag

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
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 过滤目录
EXCLUDE_DIR_OR_FILE=("cr-test-demo" "git_batch_co_brach.sh" "git_batch_co_push_remote.sh" "git_batch_co_merge_push_remote.sh")

# 分支名字
TAG_NAME=$1

# 校验参数
if [ -f $TAG_NAME ]; then
echo "Please input your tag name."
exit 0
fi

# 切换到某个分支
function doSomething() {
cd "$1"
pwd
git fetch
git checkout master
git tag -a $TAG_NAME -m "创建分支$TAG_NAME"
echo "success create tag " $TAG_NAME
git push origin --tags
echo "success push tag to remote" $TAG_NAME
}

## get all folder in "$WORK_DIR"
for i in `ls "$WORK_DIR"`;do
cd "$WORK_DIR"
if [[ -d $i ]] && [[ ! "${EXCLUDE_DIR_OR_FILE[@]}" =~ "${i}" ]];then
doSomething "$i"
fi
done

具体使用,首先切换为master分支。

1
./git_batch_tag_push.sh v1.0.0
06-批量mvn打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /bin/bash

# 目录空间
HOME_DIR="/Users/chenyuan"
WORK_DIR=$HOME_DIR"/Workspaces/xxx" // 替换为你的工作目录

# 需要deploy的工程
NEED_DEPLOY_DIR_OR_FILE=("xxxx") // 这里添加你要打包的项目名称,空格分开


## get all folder in "$WORK_DIR"
for i in "${NEED_DEPLOY_DIR_OR_FILE[@]}";do
#cd "$WORK_DIR"
echo "$WORK_DIR/$i";
cd "$WORK_DIR/$i";
git pull
mvn install deploy -Dpmd.skip=true -Dcheckstyle.skip=true -Dmaven.test.skip=true
done

具体使用

1
./git_batch_deploy_snapshot.sh

上面的这些脚本也是因为当时自己要发布版本,总共写脚本花费了20~30mins的时候,发布时间也才30mins。如果那天我不写这个脚本,业务我每次都要花费60+mins去做,而且其他不熟悉的人做发布的时候肯定会花费更多的时间。开发就应该把那些重复的事情标准化与产品化。能够真的做到可持续~

参考地址

背包问题(Knapsack Problem, KP)NP完全问题,也是一类重要 的组合优化问题 ,在工业 、经济 、通信、金融与计算机 等领域的资 源分配 、 资金预算 、 投资决策 、 装载问题 、 整数规划 、 分布式系统 与密码系统中具有重要的理论和应用价值。

通俗说法

贼,夜入豪宅,可偷之物甚多,而负重能力有限,偷哪些才更加不枉此行?

抽象说法

给定一组多个([公式])物品,每种物品都有自己的重量([公式])和价值([公式]),在限定的总重量/总容量([公式])内,选择其中若干个(也即每种物品可以选0个或1个),设计选择方案使得物品的总价值最高。

更加抽象的说法

给定正整数[公式]、给定正整数[公式],求解0-1规划问题:

[公式] , s.t. [公式][公式]

0-1背包问题的递推关系

定义子问题 [公式] 为:在前 [公式] 个物品中挑选总重量不超过 [公式] 的物品,每种物品至多只能挑选1个,使得总价值最大;这时的最优值记作 [公式] ,其中 [公式][公式]

考虑第 [公式] 个物品,无外乎两种可能:选,或者不选。

  • 不选的话,背包的容量不变,改变为问题 [公式]
  • 选的话,背包的容量变小,改变为问题 [公式]

最优方案就是比较这两种方案,哪个会更好些:

[公式]

http://static.cyblogs.com/v2-1d8090c991ca13cee3cb43c027b72304_1440w.jpg

得到

[公式]

“填二维表”的动态规划方法

[公式] 时才会有“取第 [公式] 件物品”发生。

所以从表格右下角“往回看”如果是“垂直下降”就是发生了 [公式] ,而只有“走斜线”才是“取了”物品。

http://static.cyblogs.com/v2-7bd4c72ec3b5f104e4db3c4aad98cc66_1440w.png

这个算法的复杂度就很容易算了——每一个格子都要填写数字,所以时间复杂度和空间复杂度都是 [公式] 。当” [公式] “时(就不严谨地使用渐近分析的语言了),复杂度是 [公式]

手撕Java版本代码

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
package com.cyblogs.algorithm;

/**
* Created with leetcode-cn
*
* @description: 0-1 背包问题
* @author: chenyuan
* @date: 2021/3/29
* @time: 10:23
*/
public class Knapsack {
public static void main(String[] args) {
int N = 6, W = 21;
// 重量
int[] w = {0, 2, 3, 4, 5, 9};
// 价值
int[] v = {0, 3, 4, 5, 8, 10};
// 重量 * 价值
int[][] B = new int[N][W];

int k,C;
for (k = 1; k < N; k++) {
for (C = 1; C < W; C++) {
if (w[k] > C) {
B[k][C] = B[k-1][C];
} else {
// 装进书包
int value1 = B[k-1][C-w[k]] + v[k];
// 不装进书包
int value2 = B[k-1][C];
if (value1 > value2) {
B[k][C] = value1;
} else {
B[k][C] = value2;
}
}
}
}
System.out.println(B[5][20]);
}
}

最后输出的结果是:26。

参考地址

背景

相信很多程序员对于 Linux 系统都不陌生,即使自己的日常开发机器不是 Linux,那么线上服务器也大部分都是的,所以,掌握常用的 Linux 命令也是程序员必备的技能。

但是,怕就怕很多人对于部分命令只是一知半解,使用不当就能导致线上故障。

前段时间,我们的线上应用报警,频繁 FGC,需要紧急处理问题,于是有同事去线上重启机器(正常程序应该是先采集堆 dump,然后再重启,方便排查是否存在内存泄露等问题)。

但是在重启过程中,同事发现正常的重启命令应用无反应,然后尝试使用 kill 命令 “杀” 掉 Java 进程,但是仍然无效。于是他私自决定使用 “kill -9“ 结束了进程的生命。

虽然应用进程被干掉了,但是随之而来带来了很多问题,首先是上游系统突然发生大量报警,对应开发找过来说调用我们的 RPC 服务无响应,频繁超时。

后来,我们又发现系统中存在部分脏数据,有些在同一个事务中需要完整更新的数据,只更新了一半…

为什么正常的 kill 无法 “杀掉” 进程,而 kill -9 就可以?为什么 kill -9 会引发这一连串连锁反应?正常的 kill 执行时,JVM 会如何处理的呢?

要搞清楚这些问题,我们要先从 kill 命令说起。

kill 命令

我们都知道,想要在 Linux 中终止一个进程有两种方式,如果是前台进程可以使用 Ctrl+C 键进行终止;如果是后台进程,那么需要使用 kill 命令来终止。(其实 Ctrl+C 也是 kill 命令)

kill 命令的格式是:

1
2
3
4
kill [参数] [进程号]
如:
kill 90124
kill -9 90124

其中 [参数] 是可选的,进程号可以通过 jps/ps/pidof/pstree/top 等工具获取。

kill 的命令参数有以下几种:

1
2
3
4
5
6
7
8
9
-l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称

-a 当处理当前进程时,不限制命令名和进程号的对应关系

-p 指定kill 命令只打印相关进程的进程号,而不发送任何信号

-s 指定发送信号

-u 指定用户

通常情况下,我们使用的 - l(信号) 的时候比较多,如我们前文提到的 kill -9 中的 9 就是信号。

信号如果没有指定的话,默认会发出终止信号 (15)。常用的信号如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
HUP 1 终端断线

INT 2 中断(同 Ctrl + C)

QUIT 3 退出(同 Ctrl + \)

TERM 15 终止

KILL 9 强制终止

CONT 18 继续(与STOP相反, fg/bg命令)

STOP 19 暂停(同 Ctrl + Z)

比较常用的就是强制终止信号:9终止信号:15,另外,中断信号:2 其实就是我们前文提到的 Ctrl + C 结束前台进程。

那么,kill -9kill -15 到底有什么区别呢?该如何选择呢?

kill -9 和 kill -15 的区别

kill 命令默认的信号就是 15,首先来说一下这个默认的 kill -15 信号。

当使用 kill -15 时,系统会发送一个 SIGTERM 的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的。

这时候,应用程序可以选择:

  • 1、立即停止程序
  • 2、释放响应资源后停止程序
  • 3、忽略该信号,继续执行程序

因为 kill -15 信号只是通知对应的进程要进行 “安全、干净的退出”,程序接到信号之后,退出前一般会进行一些 “准备工作”,如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。

但是,如果在 “准备工作” 进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。

这也就是为什么我们有的时候使用 kill 命令是没办法 “杀死” 应用的原因,因为默认的 kill 信号是 SIGTERM(15),而 SIGTERM(15)的信号是可以被阻塞和忽略的。

kill -15 相比,kill -9 就相对强硬一点,系统会发出 SIGKILL 信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。

所以,相比于 kill -15 命令,kill -9 在执行时,应用程序是没有时间进行 “准备工作” 的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。

Java 是如何处理 SIGTERM(15)

我们都知道,在 Linux 中,Java 应用是作为一个独立进程运行的,Java 程序的终止运行是基于 JVM 的关闭实现的,JVM 关闭方式分为 3 种:

  • 正常关闭:当最后一个非守护线程结束或者调用了 System.exit 或者通过其他特定平台的方法关闭(接收到 SIGINT(2)SIGTERM(15)信号等)
  • 强制关闭:通过调用 Runtime.halt 方法或者是在操作系统中强制 kill(接收到 SIGKILL(9)信号)
  • 异常关闭:运行中遇到 RuntimeException 异常等

JVM 进程在接收到 kill -15 信号通知的时候,是可以做一些清理动作的,比如删除临时文件等。

当然,开发者也是可以自定义做一些额外的事情的,比如让 tomcat 容器停止,让 dubbo 服务下线等。

而这种自定义 JVM 清理动作的方式,是通过 JDK 中提供的 shutdown hook 实现的。JDK 提供了 Java.Runtime.addShutdownHook(Thread hook) 方法,可以注册一个 JVM 关闭的钩子。

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
package com.cyblogs.thread;

/**
* Created with leetcode-cn
*
* @description:
* @author: chenyuan
* @date: 2021/3/28
* @time: 20:34
*/
public class ShutdownHookCase {
public static void main(String[] args) {

boolean flag = true;

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("hook execute...");
}));

while (flag) {
// app is runing
}
System.out.println("main thread execute end...");
}
}

执行命令:

http://static.cyblogs.com/Jietu20210328-204711.jpg

控制台输出内容:

1
2
hook execute...
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)

可以看到,当我们使用 kill(默认 kill -15)关闭进程的时候,程序会先执行我注册的 shutdownHook,然后再退出,并且会给出一个提示:interrupted by signal 15: SIGTERM

如果我们执行命令 kill -9

http://static.cyblogs.com/Jietu20210328-204851.jpg

控制台输出内容:

1
Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

可以看到,当我们使用 kill -9 强制关闭进程的时候,程序并没有执行 shutdownHook,而是直接退出了,并且会给出一个提示:interrupted by signal 9: SIGKILL

总结

kill 命令用于终止 Linux 进程,默认情况下,如果不指定信号,kill 等价于 kill -15

kill -15 执行时,系统向对应的程序发送 SIGTERM(15)信号,该信号是可以被执行、阻塞和忽略的,所以应用程序接收到信号后,可以做一些准备工作,再进行程序终止。

有的时候,kill -15 无法终止程序,因为他可能被忽略,这时候可以使用 kill -9,系统会发出 SIGKILL(9)信号,该信号不允许忽略和阻塞,所以应用程序会立即终止。

这也会带来很多副作用,如数据丢失等,所以,在非必要时,不要使用 kill -9 命令,尤其是那些 web 应用、提供 RPC 服务、执行定时任务、包含长事务等应用中,因为 kill -9 没给 spring 容器、tomcat 服务器、dubbo 服务、流程引擎、状态机等足够的时间进行收尾。

参考地址

0%